/*
* 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;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import com.google.inject.Guice;
import com.google.inject.Injector;
import io.bitsquare.alert.AlertManager;
import io.bitsquare.arbitration.ArbitratorManager;
import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService;
import io.bitsquare.common.CommonOptionKeys;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.common.util.LimitedKeyStrengthException;
import io.bitsquare.common.util.Profiler;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.filter.FilterManager;
import io.bitsquare.gui.SystemTray;
import io.bitsquare.gui.common.UITimer;
import io.bitsquare.gui.common.view.CachingViewLoader;
import io.bitsquare.gui.common.view.View;
import io.bitsquare.gui.common.view.ViewLoader;
import io.bitsquare.gui.common.view.guice.InjectorViewFactory;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.MainViewModel;
import io.bitsquare.gui.main.debug.DebugView;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.*;
import io.bitsquare.gui.util.ImageUtil;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.storage.Storage;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.offer.OpenOfferManager;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.bitcoinj.store.BlockStoreException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.controlsfx.dialog.Dialogs;
import org.reactfx.EventStreams;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static io.bitsquare.app.AppOptionKeys.APP_NAME_KEY;
public class BitsquareApp extends Application {
private static final Logger log = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(BitsquareApp.class);
private static final long LOG_MEMORY_PERIOD_MIN = 10;
private static Environment env;
private BitsquareAppModule bitsquareAppModule;
private Injector injector;
private boolean popupOpened;
private static Stage primaryStage;
private Scene scene;
private final List<String> corruptedDatabaseFiles = new ArrayList<>();
private MainView mainView;
public static Runnable shutDownHandler;
private boolean shutDownRequested;
public static void setEnvironment(Environment env) {
BitsquareApp.env = env;
}
@Override
public void start(Stage stage) throws IOException {
BitsquareApp.primaryStage = stage;
String logPath = Paths.get(env.getProperty(AppOptionKeys.APP_DATA_DIR_KEY), "bitsquare").toString();
Log.setup(logPath);
log.info("Log files under: " + logPath);
Version.printVersion();
Utilities.printSysInfo();
Log.setLevel(Level.toLevel(env.getRequiredProperty(CommonOptionKeys.LOG_LEVEL_KEY)));
UserThread.setExecutor(Platform::runLater);
UserThread.setTimerClass(UITimer.class);
shutDownHandler = this::stop;
// setup UncaughtExceptionHandler
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
// Might come from another thread
if (throwable.getCause() != null && throwable.getCause().getCause() != null &&
throwable.getCause().getCause() instanceof BlockStoreException) {
log.error(throwable.getMessage());
} else if (throwable instanceof ClassCastException &&
"sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else {
log.error("Uncaught Exception from thread " + Thread.currentThread().getName());
log.error("throwableMessage= " + throwable.getMessage());
log.error("throwableClass= " + throwable.getClass());
log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable));
throwable.printStackTrace();
UserThread.execute(() -> showErrorPopup(throwable, false));
}
};
Thread.setDefaultUncaughtExceptionHandler(handler);
Thread.currentThread().setUncaughtExceptionHandler(handler);
try {
Utilities.checkCryptoPolicySetup();
} catch (NoSuchAlgorithmException | LimitedKeyStrengthException e) {
e.printStackTrace();
UserThread.execute(() -> showErrorPopup(e, true));
}
Security.addProvider(new BouncyCastleProvider());
try {
// Guice
bitsquareAppModule = new BitsquareAppModule(env, primaryStage);
injector = Guice.createInjector(bitsquareAppModule);
injector.getInstance(InjectorViewFactory.class).setInjector(injector);
Version.setBtcNetworkId(injector.getInstance(BitsquareEnvironment.class).getBitcoinNetwork().ordinal());
if (Utilities.isLinux())
System.setProperty("prism.lcdtext", "false");
Storage.setDatabaseCorruptionHandler((String fileName) -> {
corruptedDatabaseFiles.add(fileName);
if (mainView != null)
mainView.setPersistedFilesCorrupted(corruptedDatabaseFiles);
});
// load the main view and create the main scene
CachingViewLoader viewLoader = injector.getInstance(CachingViewLoader.class);
mainView = (MainView) viewLoader.load(MainView.class);
mainView.setPersistedFilesCorrupted(corruptedDatabaseFiles);
/* Storage.setDatabaseCorruptionHandler((String fileName) -> {
corruptedDatabaseFiles.add(fileName);
if (mainView != null)
mainView.setPersistedFilesCorrupted(corruptedDatabaseFiles);
});*/
scene = new Scene(mainView.getRoot(), 1200, 700); //740
Font.loadFont(getClass().getResource("/fonts/Verdana.ttf").toExternalForm(), 13);
Font.loadFont(getClass().getResource("/fonts/VerdanaBold.ttf").toExternalForm(), 13);
Font.loadFont(getClass().getResource("/fonts/VerdanaItalic.ttf").toExternalForm(), 13);
Font.loadFont(getClass().getResource("/fonts/VerdanaBoldItalic.ttf").toExternalForm(), 13);
scene.getStylesheets().setAll(
"/io/bitsquare/gui/bitsquare.css",
"/io/bitsquare/gui/images.css",
"/io/bitsquare/gui/CandleStickChart.css");
// configure the system tray
SystemTray.create(primaryStage, shutDownHandler);
primaryStage.setOnCloseRequest(event -> {
event.consume();
stop();
});
scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> {
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent) || new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
stop();
} else if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(keyEvent) || new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
stop();
} else if (new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN).match(keyEvent) || new KeyCodeCombination(KeyCode.E, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
showEmptyWalletPopup();
} else if (new KeyCodeCombination(KeyCode.M, KeyCombination.ALT_DOWN).match(keyEvent)) {
showSendAlertMessagePopup();
} else if (new KeyCodeCombination(KeyCode.F, KeyCombination.ALT_DOWN).match(keyEvent)) {
showFilterPopup();
} else if (new KeyCodeCombination(KeyCode.F, KeyCombination.ALT_DOWN).match(keyEvent)) {
showFPSWindow();
} else if (new KeyCodeCombination(KeyCode.J, KeyCombination.ALT_DOWN).match(keyEvent)) {
WalletService walletService = injector.getInstance(WalletService.class);
if (walletService.getWallet() != null)
new ShowWalletDataWindow(walletService).information("Wallet raw data").show();
else
new Popup<>().warning("The wallet is not initialized yet").show();
} else if (new KeyCodeCombination(KeyCode.G, KeyCombination.ALT_DOWN).match(keyEvent)) {
TradeWalletService tradeWalletService = injector.getInstance(TradeWalletService.class);
WalletService walletService = injector.getInstance(WalletService.class);
if (walletService.getWallet() != null)
new SpendFromDepositTxWindow(tradeWalletService).information("Emergency wallet tool").show();
else
new Popup<>().warning("The wallet is not initialized yet").show();
} else if (DevFlags.DEV_MODE && new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
showDebugWindow();
}
});
// configure the primary stage
primaryStage.setTitle(env.getRequiredProperty(APP_NAME_KEY));
primaryStage.setScene(scene);
primaryStage.setMinWidth(1000); // 1190
primaryStage.setMinHeight(620);
// on windows the title icon is also used as task bar icon in a larger size
// on Linux no title icon is supported but also a large task bar icon is derived from that title icon
String iconPath;
if (Utilities.isOSX())
iconPath = ImageUtil.isRetina() ? "/images/window_icon@2x.png" : "/images/window_icon.png";
else if (Utilities.isWindows())
iconPath = "/images/task_bar_icon_windows.png";
else
iconPath = "/images/task_bar_icon_linux.png";
primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(iconPath)));
// make the UI visible
primaryStage.show();
if (!Utilities.isCorrectOSArchitecture()) {
String osArchitecture = Utilities.getOSArchitecture();
// We don't force a shutdown as the osArchitecture might in strange cases return a wrong value.
// Needs at least more testing on different machines...
new Popup<>().warning("You probably have the wrong Bitsquare version for this computer.\n" +
"Your computer's architecture is: " + osArchitecture + ".\n" +
"The Bitsquare binary you installed is: " + Utilities.getJVMArchitecture() + ".\n" +
"Please shut down and re-install the correct version (" + osArchitecture + ").")
.show();
}
UserThread.runPeriodically(() -> Profiler.printSystemLoad(log), LOG_MEMORY_PERIOD_MIN, TimeUnit.MINUTES);
} catch (
Throwable throwable
)
{
showErrorPopup(throwable, false);
}
}
private void showSendAlertMessagePopup() {
AlertManager alertManager = injector.getInstance(AlertManager.class);
new SendAlertMessageWindow()
.onAddAlertMessage(alertManager::addAlertMessageIfKeyIsValid)
.onRemoveAlertMessage(alertManager::removeAlertMessageIfKeyIsValid)
.show();
}
private void showFilterPopup() {
FilterManager filterManager = injector.getInstance(FilterManager.class);
new FilterWindow(filterManager)
.onAddFilter(filterManager::addFilterMessageIfKeyIsValid)
.onRemoveFilter(filterManager::removeFilterMessageIfKeyIsValid)
.show();
}
private void showEmptyWalletPopup() {
injector.getInstance(EmptyWalletWindow.class).show();
}
private void showErrorPopup(Throwable throwable, boolean doShutDown) {
if (!shutDownRequested) {
if (scene == null) {
log.warn("Scene not available yet, we create a new scene. The bug might be caused by an exception in a constructor or by a circular dependency in guice.");
scene = new Scene(new StackPane(), 1000, 650);
scene.getStylesheets().setAll(
"/io/bitsquare/gui/bitsquare.css",
"/io/bitsquare/gui/images.css");
primaryStage.setScene(scene);
primaryStage.show();
}
try {
try {
if (!popupOpened) {
String message = throwable.getMessage();
popupOpened = true;
if (message != null)
new Popup().error(message).onClose(() -> popupOpened = false).show();
else
new Popup().error(throwable.toString()).onClose(() -> popupOpened = false).show();
}
} catch (Throwable throwable3) {
log.error("Error at displaying Throwable.");
throwable3.printStackTrace();
}
if (doShutDown)
stop();
} catch (Throwable throwable2) {
// If printStackTrace cause a further exception we don't pass the throwable to the Popup.
Dialogs.create()
.owner(primaryStage)
.title("Error")
.message(throwable.toString())
.masthead("A fatal exception occurred at startup.")
.showError();
if (doShutDown)
stop();
}
}
}
// Used for debugging trade process
private void showDebugWindow() {
ViewLoader viewLoader = injector.getInstance(ViewLoader.class);
View debugView = viewLoader.load(DebugView.class);
Parent parent = (Parent) debugView.getRoot();
Stage stage = new Stage();
stage.setScene(new Scene(parent));
stage.setTitle("Debug window");
stage.initModality(Modality.NONE);
stage.initStyle(StageStyle.UTILITY);
stage.initOwner(scene.getWindow());
stage.setX(primaryStage.getX() + primaryStage.getWidth() + 10);
stage.setY(primaryStage.getY());
stage.show();
}
private void showFPSWindow() {
Label label = new Label();
EventStreams.animationTicks()
.latestN(100)
.map(ticks -> {
int n = ticks.size() - 1;
return n * 1_000_000_000.0 / (ticks.get(n) - ticks.get(0));
})
.map(d -> String.format("FPS: %.3f", d))
.feedTo(label.textProperty());
Pane root = new StackPane();
root.getChildren().add(label);
Stage stage = new Stage();
stage.setScene(new Scene(root));
stage.setTitle("FPS");
stage.initModality(Modality.NONE);
stage.initStyle(StageStyle.UTILITY);
stage.initOwner(scene.getWindow());
stage.setX(primaryStage.getX() + primaryStage.getWidth() + 10);
stage.setY(primaryStage.getY());
stage.setWidth(200);
stage.setHeight(100);
stage.show();
}
@Override
public void stop() {
if (!shutDownRequested) {
new Popup().headLine("Shut down in progress")
.backgroundInfo("Shutting down application can take a few seconds.\n" +
"Please don't interrupt this process.")
.hideCloseButton()
.useAnimation(false)
.show();
UserThread.runAfter(() -> {
gracefulShutDown(() -> {
log.debug("App shutdown complete");
System.exit(0);
});
}, 200, TimeUnit.MILLISECONDS);
shutDownRequested = true;
}
}
private void gracefulShutDown(ResultHandler resultHandler) {
log.debug("gracefulShutDown");
try {
if (injector != null) {
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(MainViewModel.class).shutDown();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
injector.getInstance(P2PService.class).shutDown(() -> {
injector.getInstance(WalletService.class).shutDownDone.addListener((ov, o, n) -> {
bitsquareAppModule.close(injector);
log.debug("Graceful shutdown completed");
resultHandler.handleResult();
});
injector.getInstance(WalletService.class).shutDown();
});
});
// we wait max 20 sec.
UserThread.runAfter(resultHandler::handleResult, 20);
} else {
UserThread.runAfter(resultHandler::handleResult, 1);
}
} catch (Throwable t) {
log.debug("App shutdown failed with exception");
t.printStackTrace();
System.exit(1);
}
}
}