package lighthouse;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.Uninterruptibles;
import com.vinumeris.updatefx.Crypto;
import com.vinumeris.updatefx.UpdateFX;
import javafx.animation.Animation;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import lighthouse.controls.NotificationBarPane;
import lighthouse.files.AppDirectory;
import lighthouse.protocol.LHUtils;
import lighthouse.subwindows.EmbeddedWindow;
import lighthouse.utils.GuiUtils;
import lighthouse.utils.TextFieldValidator;
import lighthouse.wallet.PledgingWallet;
import org.bitcoinj.core.*;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.net.discovery.PeerDiscovery;
import org.bitcoinj.net.discovery.PeerDiscoveryException;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.DeterministicSeed;
import org.bouncycastle.math.ec.ECPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import static lighthouse.LighthouseBackend.Mode.CLIENT;
import static lighthouse.protocol.LHUtils.uncheck;
import static lighthouse.protocol.LHUtils.unchecked;
import static lighthouse.utils.GuiUtils.*;
public class Main extends Application {
public static final Logger log = LoggerFactory.getLogger(Main.class);
// This is an UpdateFX version code. It's incremented monotonically after a new version is released via
// auto update.
public static final int VERSION = 12;
public static final String APP_NAME = "Lighthouse";
public static final String UPDATES_BASE_URL = "https://www.vinumeris.com/lighthouse/updates";
public static final List<ECPoint> UPDATE_SIGNING_KEYS = Crypto.decode("02A3CDE5D0EDC281637C67AA67C0CB009EA6573E0F101C6E018ACB91393C08C129");
public static final int UPDATE_SIGNING_THRESHOLD = 1;
public static NetworkParameters params;
public static volatile WalletAppKit bitcoin;
public static PledgingWallet wallet;
public static Main instance;
public static String demoName;
public static LighthouseBackend backend;
private StackPane uiStack;
private Pane mainUI;
public Scene scene;
public Stage mainStage;
public MainWindow mainWindow;
private CountDownLatch walletLoadedLatch = new CountDownLatch(1);
public NotificationBarPane notificationBar;
public UserPrefs prefs;
private boolean useTor;
private boolean slowGFX;
public String updatesURL = UPDATES_BASE_URL;
public static void main(String[] args) throws IOException {
// Startup sequence: we use UpdateFX to allow for automatic online updates when the app is running. UpdateFX
// applies binary deltas to our JAR to create a new JAR that's then dropped into the app directory.
// Here we call into UpdateFX to ask it to locate the JAR with the highest version and then load it up.
// Therefore this and realMain are the only methods that will *always* run: other parts of the code may not do
// if they've been replaced by a higher version. Equally, this is the only part we can't upgrade automatically
// so we need to do as little as possible here.
AppDirectory.initAppDir(APP_NAME);
UpdateFX.bootstrap(Main.class, AppDirectory.dir(), args); // -> takes us to realMain, maybe in a newer version
}
// Need to pin this to work around a questionable design change in the JDK where it uses weak refs to hold loggers.
private static boolean logToConsole = false;
private static java.util.logging.Logger logger;
private static void setupLogging() {
logger = java.util.logging.Logger.getLogger("");
final FileHandler handler = unchecked(() -> new FileHandler(AppDirectory.dir().resolve("log.txt").toString(), true));
handler.setFormatter(new BriefLogFormatter());
logger.addHandler(handler);
if (logToConsole) {
logger.getHandlers()[0].setFormatter(new BriefLogFormatter());
} else {
logger.removeHandler(logger.getHandlers()[0]);
}
}
public static void realMain(String[] args) {
launch(args); // -> takes us to start() on a new thread, with JFX initialised.
}
@Override
public void start(Stage stage) throws Exception {
// For some reason the JavaFX launch process results in us losing the thread context class loader: reset it.
Thread.currentThread().setContextClassLoader(Main.class.getClassLoader());
instance = this;
// Show the crash dialog for any exceptions that we don't handle and that hit the main loop.
handleCrashesOnThisThread();
// Set up the app dir + logging again, because the first time we did this (in main) could have been in the
// context of another class loader if we're now running a different §app version to the one the user installed.
// Anything that happened in main() therefore might have now been wipedt.
AppDirectory.initAppDir(APP_NAME);
if (!parseCommandLineArgs()) {
Platform.exit();
return;
}
setupLogging();
log.info("\n\nLighthouse {} starting up. It is {}\n", VERSION, LHUtils.nowAsString());
log.info("App dir is {}. We have {} cores.", AppDirectory.dir(), Runtime.getRuntime().availableProcessors());
// Set up the basic window with an empty UI stack, and put a quick splash there.
reached("JFX initialised");
prefs = new UserPrefs();
initGUI(stage);
stage.show();
Runnable setup = () -> {
uncheck(() -> initBitcoin(null)); // This will happen mostly async.
loadMainWindow();
};
runOnGuiThreadAfter(300, setup);
}
private boolean parseCommandLineArgs() {
if (getParameters().getUnnamed().contains("--help") || getParameters().getUnnamed().contains("-h")) {
System.out.println(String.format(
"Lighthouse version %d (C) 2014 Vinumeris GmbH%n%n" +
"Usage:%n" +
" --use-tor: Enable experimental Tor mode (may freeze up)%n" +
" --slow-gfx: Enable more eyecandy that may stutter on slow GFX cards%n" +
" --net={regtest,main,testnet}: Select Bitcoin network to operate on.%n" +
" --name=alice Name is put in titlebar and pledge filenames, useful for testing%n" +
" multiple instances on the same machine.%n" +
" --appdir=/path/to/dir Overrides the usual directory used, useful for testing multiple%n" +
" instances on the same machine.%n" +
" --debuglog Print logging data to the console.%n" +
" --updates-url=http://xxx/ Override the default URL used for updates checking.%n" +
" --resfiles=/path/to/dir Load GUI resource files from the given directory instead of using%n" +
" the included versions.%n",
VERSION
));
return false;
}
useTor = getParameters().getUnnamed().contains("--use-tor");
// TODO: Auto-detect adapter and configure this.
slowGFX = !getParameters().getUnnamed().contains("--slow-gfx");
// TODO: Reset to "production"
String netname = "test";
if (getParameters().getNamed().containsKey("net")) // e.g. regtest or testnet
netname = getParameters().getNamed().get("net");
params = NetworkParameters.fromID("org.bitcoin." + netname);
if (params == null) {
informationalAlert("Unknown network ID", "The --net parameter must be production, regtest or testnet");
return false;
}
demoName = getParameters().getNamed().get("name");
String dir = getParameters().getNamed().get("appdir");
if (dir != null) {
AppDirectory.overrideAppDir(Paths.get(dir));
uncheck(() -> AppDirectory.initAppDir(APP_NAME));
}
logToConsole = getParameters().getUnnamed().contains("--debuglog");
String updatesURL = getParameters().getNamed().get("updates-url");
if (updatesURL != null) {
if (LHUtils.didThrow(() -> new URI(updatesURL))) {
informationalAlert("Bad updates URL", "The --updates-url parameter is invalid: %s", updatesURL);
return false;
}
this.updatesURL = updatesURL;
}
String resdir = getParameters().getNamed().get("resdir");
if (resdir != null) {
GuiUtils.resourceOverrideDirectory = Paths.get(resdir);
if (!Files.isDirectory(GuiUtils.resourceOverrideDirectory) ||
!Files.exists(GuiUtils.resourceOverrideDirectory.resolve("main.fxml"))) {
informationalAlert("Not a directory", "The --resdir value must point to a directory containing UI resource files (fxml, css, etc).");
return false;
}
}
return true;
}
private static Stopwatch globalStopwatch = Stopwatch.createStarted();
private static ThreadLocal<Stopwatch> threadStopwatch = ThreadLocal.withInitial(Stopwatch::createStarted);
private static ThreadLocal<Long> last = ThreadLocal.withInitial(() -> 0L);
public static void reached(String s) {
final long elapsed = threadStopwatch.get().elapsed(TimeUnit.MILLISECONDS);
log.info("[{}ms / {}ms / {}ms] {}", globalStopwatch.elapsed(TimeUnit.MILLISECONDS), elapsed, elapsed - last.get(), s);
last.set(elapsed);
}
private void initGUI(Stage stage) throws IOException {
if (GuiUtils.isSoftwarePipeline())
log.warn("Prism is using software rendering");
mainStage = stage;
stage.getIcons().add(new Image(Main.class.getResourceAsStream("icon.png")));
Font.loadFont(Main.class.getResource("nanlight-webfont.ttf").toString(), 10);
Font.loadFont(Main.class.getResource("nanlightbold-webfont.ttf").toString(), 10);
// Create the scene with a StackPane so we can overlay things on top of the main UI.
final Node loadingUI = createLoadingUI();
uiStack = new StackPane(loadingUI);
scene = new Scene(uiStack);
scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+S"), () -> Platform.runLater(this::loadMainWindow));
stage.setTitle(APP_NAME);
stage.setMinWidth(800);
stage.setMinHeight(600);
stage.setWidth(1024);
// Kind of empirical hacks - we could also just start maximised but then animations can get slow on big retina
// displays. We try and fit on small screens with task bars / docks here.
stage.setHeight(Math.max(750, Screen.getPrimary().getBounds().getHeight() - 150));
stage.setScene(scene);
if (demoName != null) {
stage.setTitle(APP_NAME + " - " + demoName);
}
}
private Node createLoadingUI() {
ImageView lighthouseLogo = new ImageView(getResource("Logo.jpg").toString());
lighthouseLogo.setFitWidth(500);
lighthouseLogo.setPreserveRatio(true);
StackPane.setAlignment(lighthouseLogo, Pos.CENTER);
// ImageView vinumerisLogo = new ImageView(GuiUtils.getResource("vinumeris.png").toString());
// vinumerisLogo.setFitHeight(25);
// vinumerisLogo.setPreserveRatio(true);
// StackPane.setAlignment(vinumerisLogo, Pos.TOP_RIGHT);
// StackPane pane = new StackPane(lighthouseLogo, vinumerisLogo);
StackPane pane = new StackPane(lighthouseLogo);
pane.setPadding(new Insets(20));
pane.setStyle("-fx-background-color: white");
return pane;
}
private boolean firstRun = true;
private void loadMainWindow() {
try {
refreshStylesheets(scene);
// Load the main window.
URL location = getResource("main.fxml");
FXMLLoader loader = new FXMLLoader(location);
Pane ui = LHUtils.stopwatched("Loading main.fxml", loader::load);
ui.setMaxWidth(Double.MAX_VALUE);
ui.setMaxHeight(Double.MAX_VALUE);
MainWindow controller = loader.getController();
// Embed it into a notification pane.
notificationBar = new NotificationBarPane(ui);
mainUI = notificationBar;
mainWindow = controller;
Uninterruptibles.awaitUninterruptibly(walletLoadedLatch);
if (bitcoin == null)
return;
mainUI.setCache(true);
reached("Showing main UI");
uiStack.getChildren().add(0, mainUI);
mainWindow.onBitcoinSetup();
// When the app has just loaded fade out. If we're doing development and force refreshing the UI with a
// hotkey, don't fade, it just slows me down.
final Node node = uiStack.getChildren().get(1);
if (firstRun) {
fadeOutAndRemove(Duration.millis(slowGFX ? UI_ANIMATION_TIME_MSEC * 2 : UI_ANIMATION_TIME_MSEC), uiStack, node);
firstRun = false;
} else {
uiStack.getChildren().remove(node);
}
} catch (Throwable e) {
log.error("Failed to load UI: ", e);
if (GuiUtils.resourceOverrideDirectory != null)
informationalAlert("Failed to load UI", "Error: %s", e.getMessage());
else
GuiUtils.crashAlert(e);
}
}
public void initBitcoin(@Nullable DeterministicSeed restoreFromSeed) throws IOException {
// Tell bitcoinj to run event handlers on the JavaFX UI thread. This keeps things simple and means
// we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener
// we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in
// a future version.
Threading.USER_THREAD = Platform::runLater;
// Create the app kit. It won't do any heavyweight initialization until after we start it.
bitcoin = new WalletAppKit(params, AppDirectory.dir().toFile(), APP_NAME) {
{
walletFactory = PledgingWallet::new;
}
@Override
protected void onSetupCompleted() {
handleCrashesOnThisThread();
wallet = (PledgingWallet) bitcoin.wallet();
backend = new LighthouseBackend(CLIENT, vPeerGroup, vChain, wallet);
reached("onSetupCompleted");
walletLoadedLatch.countDown();
if (params == RegTestParams.get()) {
vPeerGroup.setMaxConnections(1);
} else {
// TODO: Replace this with a DNS seed that crawls for NODE_GETUTXOS (or whatever it's renamed to).
PeerDiscovery hardCodedPeers = new PeerDiscovery() {
@Override
public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException {
InetSocketAddress[] result = new InetSocketAddress[2];
result[0] = new InetSocketAddress("vinumeris.com", params.getPort());
result[1] = new InetSocketAddress("riker.plan99.net", params.getPort());
return result;
}
@Override
public void shutdown() {
}
};
vPeerGroup.addPeerDiscovery(hardCodedPeers);
vPeerGroup.setMaxConnections(2);
vPeerGroup.setConnectTimeoutMillis(10000);
// Sequence things so we *always* get both hard-coded peers for now.
vPeerGroup.waitForPeersOfVersion(2, GetUTXOsMessage.MIN_PROTOCOL_VERSION).addListener(() -> {
vPeerGroup.addPeerDiscovery(new DnsDiscovery(params));
vPeerGroup.setMaxConnections(6);
}, Threading.SAME_THREAD);
}
vPeerGroup.addEventListener(new AbstractPeerEventListener() {
@Override
public void onPeerConnected(Peer peer, int peerCount) {
if (peer.getAddress().getAddr().isLoopbackAddress() && !peer.getPeerVersionMessage().isGetUTXOsSupported()) {
// We connected to localhost but it doesn't have what we need.
log.warn("Localhost peer does not have support for NODE_GETUTXOS, ignoring");
// TODO: Once Bitcoin Core fork name is chosen and released, be more specific in this message.
informationalAlert("Local Bitcoin node not usable",
"You have a Bitcoin (Core) node running on your computer, but it doesn't have the protocol support Lighthouse needs. Lighthouse will still " +
"work but will use the peer to peer network instead, so you won't get upgraded security.");
vPeerGroup.setUseLocalhostPeerWhenPossible(false);
vPeerGroup.setMaxConnections(4);
}
}
});
}
};
if (bitcoin.isChainFileLocked()) {
informationalAlert("Already running",
"This application is already running and cannot be started twice.");
Platform.exit();
bitcoin = null;
walletLoadedLatch.countDown();
return;
}
if (params == MainNetParams.get()) {
// Checkpoints are block headers that ship inside our app: for a new user, we pick the last header
// in the checkpoints file and then download the rest from the network. It makes things much faster.
// Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the
// last months worth or more (takes a few seconds).
bitcoin.setCheckpoints(getClass().getResourceAsStream("checkpoints"));
} else if (params == TestNet3Params.get()) {
bitcoin.setCheckpoints(getClass().getResourceAsStream("checkpoints.testnet"));
}
bitcoin.setPeerNodes(new PeerAddress[0]) // Hack to prevent WAK adding DnsDiscovery
.setBlockingStartup(false)
.setDownloadListener(MainWindow.bitcoinUIModel.getDownloadListener())
.setUserAgent("Lighthouse", "" + VERSION)
.restoreWalletFromSeed(restoreFromSeed);
if (useTor && params != RegTestParams.get())
bitcoin.useTor();
reached("Starting bitcoin init");
bitcoin.addListener(new Service.Listener() {
@Override
public void failed(Service.State from, Throwable failure) {
bitcoin = null;
walletLoadedLatch.countDown();
crashAlert(failure);
}
}, Threading.SAME_THREAD);
bitcoin.startAsync();
}
private void refreshStylesheets(Scene scene) {
scene.getStylesheets().clear();
TextFieldValidator.configureScene(scene);
// Generic styles first, then app specific styles.
scene.getStylesheets().add(getResource("vinumeris-style.css").toString());
scene.getStylesheets().add(getResource("main.css").toString());
}
private Node stopClickPane = new Pane();
public boolean waitForInit() {
log.info("Waiting for bitcoin load ...");
Uninterruptibles.awaitUninterruptibly(walletLoadedLatch);
if (Main.backend != null) {
log.info("Waiting for backend init ...");
Main.backend.waitForInit();
return true;
} else {
return false;
}
}
public static void restart() {
uncheck(UpdateFX::restartApp);
}
public class OverlayUI<T> {
public Node ui;
public T controller;
public OverlayUI(Node ui, T controller) {
this.ui = ui;
this.controller = controller;
}
public void show() {
checkGuiThread();
if (currentOverlay == null) {
uiStack.getChildren().add(stopClickPane);
uiStack.getChildren().add(ui);
// Workaround for crappy integrated graphics chips.
if (slowGFX) {
brightnessAdjust(mainUI, 0.9);
} else {
stopClickPane.setStyle("-fx-background-color: white");
stopClickPane.setOpacity(0.0);
fadeIn(stopClickPane, 0, 0.7);
blurOut(mainUI);
}
ui.setOpacity(0.0);
fadeIn(ui);
zoomIn(ui);
} else {
// Do a quick transition between the current overlay and the next.
// Bug here: we don't pay attention to changes in outsideClickDismisses.
explodeOut(currentOverlay.ui);
fadeOutAndRemove(uiStack, currentOverlay.ui);
uiStack.getChildren().add(ui);
ui.setOpacity(0.0);
fadeIn(ui, 100, 1.0);
zoomIn(ui, 100);
}
currentOverlay = this;
}
public void outsideClickDismisses() {
stopClickPane.setOnMouseClicked((ev) -> done());
}
private LinkedList<EventHandler<ActionEvent>> afterFade = new LinkedList<>();
@Nullable
public Animation done() {
checkGuiThread();
if (ui == null) return null; // In the middle of being dismissed and got an extra click.
explodeOut(ui);
Animation animation = fadeOutAndRemove(uiStack, ui, stopClickPane);
for (EventHandler<ActionEvent> handler : afterFade) {
EventHandler<ActionEvent> prev = animation.getOnFinished();
animation.setOnFinished(ev -> {
prev.handle(ev);
handler.handle(ev);
});
}
if (slowGFX) {
brightnessUnadjust(mainUI);
} else {
// Make the blur finish ever so slightly before the white stopclickpane is totally clear.
// This looks slightly nicer, but may not be needed after 8u20 as a bug in GaussianBlur was fixed.
blurIn(mainUI, UI_ANIMATION_TIME.multiply(0.8));
}
this.ui = null;
this.controller = null;
currentOverlay = null;
return animation;
}
public void runAfterFade(EventHandler<ActionEvent> runnable) {
afterFade.add(runnable);
}
}
@Nullable
private OverlayUI currentOverlay;
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<T>(node, controller);
// Auto-magically set the overlayUi member, if it's there.
try {
controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
}
pair.show();
return pair;
}
public <T> OverlayUI<T> overlayUI(String name) {
return overlayUI(name, null);
}
// TODO: Make title be loaded from FXML.
/** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */
public <T> OverlayUI<T> overlayUI(String name, @Nullable String title) {
try {
checkGuiThread();
// Load the UI from disk.
URL location = getResource(name);
FXMLLoader loader = new FXMLLoader(location);
Pane ui = loader.load();
T controller = loader.getController();
EmbeddedWindow window = null;
if (title != null)
ui = window = new EmbeddedWindow(title, ui);
OverlayUI<T> pair = new OverlayUI<T>(ui, controller);
// Auto-magically set the overlayUi member, if it's there.
try {
if (controller != null)
controller.getClass().getField("overlayUI").set(controller, pair);
} catch (IllegalAccessException | NoSuchFieldException ignored) {
ignored.printStackTrace();
}
if (window != null)
window.setOnCloseClicked(pair::done);
pair.show();
return pair;
} catch (IOException e) {
throw new RuntimeException(e); // Can't happen.
}
}
@Override
public void stop() throws Exception {
if (bitcoin != null && bitcoin.isRunning()) {
bitcoin.stopAsync();
bitcoin.awaitTerminated();
}
super.stop();
}
}