1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 10:39:19 +01:00

ZMQ connection monitoring (#57)

* made zeromq listener non-blocking and monitors connection status
* now throwing an exception at startup in case of zmq connection issues
* (gui) added a blocking modal in main window for ZMQ events
* made boot error exit the application in headless mode
This commit is contained in:
dpad85 2017-04-05 15:24:05 +02:00 committed by Pierre-Marie Padiou
parent edefb8d235
commit e18fcbab47
12 changed files with 390 additions and 255 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -171,3 +171,15 @@
-fx-text-fill: rgb(166,169,171);
-fx-padding: 2px 7px 0 0;
}
/* --------------- Blocker modal ----------------- */
.blocker-cover {
-fx-background-color: rgba(0,0,0,0.3);
}
.blocker-dialog {
-fx-padding: 15px;
-fx-border-width: 1px;
-fx-border-color: #888888;
-fx-background-color: #f4f4f4;
}

View File

@ -6,198 +6,223 @@
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.scene.shape.Rectangle?>
<BorderPane fx:id="root" minHeight="300.0" prefHeight="400.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<center>
<TabPane tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="channelsTab" closable="false" text="Local Channels">
<content>
<StackPane>
<children>
<ScrollPane fitToWidth="true" styleClass="channel-container">
<content>
<VBox fx:id="channelBox"/>
</content>
</ScrollPane>
<VBox fx:id="channelInfo" alignment="TOP_CENTER" styleClass="channels-info">
<AnchorPane fx:id="root" minHeight="300.0" prefHeight="400.0" styleClass="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<BorderPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.topAnchor="0" AnchorPane.bottomAnchor="0">
<center>
<TabPane tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="channelsTab" closable="false" text="Local Channels">
<content>
<StackPane>
<children>
<Label styleClass="text-strong" text="No channels opened yet..."/>
<Label styleClass="text-muted"
text="You can open a new channel by clicking on &quot;Channels&quot; &gt; &quot;Open Channel...&quot;"
wrapText="true"/>
<ScrollPane fitToWidth="true" styleClass="channel-container">
<content>
<VBox fx:id="channelBox"/>
</content>
</ScrollPane>
<VBox fx:id="channelInfo" alignment="TOP_CENTER" styleClass="channels-info">
<children>
<Label styleClass="text-strong" text="No channels opened yet..."/>
<Label styleClass="text-muted"
text="You can open a new channel by clicking on &quot;Channels&quot; &gt; &quot;Open Channel...&quot;"
wrapText="true"/>
</children>
</VBox>
</children>
</StackPane>
</content>
</Tab>
<Tab text="All Nodes" fx:id="networkNodesTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
<TableView fx:id="networkNodesTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkNodesRGBColumn" minWidth="20.0" prefWidth="20.0" maxWidth="20.0" text="" sortable="false"/>
<TableColumn fx:id="networkNodesAliasColumn" minWidth="80.0" prefWidth="180.0" maxWidth="300.0" text="Alias"/>
<TableColumn fx:id="networkNodesIdColumn" text="Node Id"/>
<TableColumn fx:id="networkNodesIPColumn" minWidth="150.0" prefWidth="250.0" maxWidth="300.0" text="IP"/>
</columns>
</TableView>
</children>
</VBox>
</children>
</StackPane>
</content>
</Tab>
<Tab text="All Nodes" fx:id="networkNodesTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
</content>
</Tab>
<Tab text="All Channels" fx:id="networkChannelsTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
<children>
<TableView fx:id="networkChannelsTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkChannelsIdColumn" minWidth="120.0" prefWidth="170.0" maxWidth="300.0" text="Channel Id"/>
<TableColumn fx:id="networkChannelsNode1Column" text="Node 1"/>
<TableColumn fx:id="networkChannelsNode2Column" text="Node 2"/>
</columns>
</TableView>
</children>
</VBox>
</content>
</Tab>
<Tab text="Activity" closable="false">
<content>
<AnchorPane>
<children>
<TabPane AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" AnchorPane.bottomAnchor="0.0"
styleClass="activities-tab" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="paymentSentTab" closable="false" text="Sent">
<TableView fx:id="paymentSentTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentSentDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentSentAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentFeesColumn" text="Fees Paid (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentReceivedTab" closable="false" text="Received">
<TableView fx:id="paymentReceivedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentReceivedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentReceivedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentReceivedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentRelayedTab" closable="false" text="Relayed">
<TableView fx:id="paymentRelayedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentRelayedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentRelayedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedFeesColumn" text="Fees Earned (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
</tabs>
</TabPane>
<Label AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" textAlignment="RIGHT"
maxWidth="180.0" wrapText="true" styleClass="activity-disclaimer"
text="Payment history will be erased when the node is shutdown." />
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
</center>
<bottom>
<HBox fx:id="statusBarBox" styleClass="status-bar" spacing="10">
<children>
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
<children>
<TableView fx:id="networkNodesTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkNodesRGBColumn" minWidth="20.0" prefWidth="20.0" maxWidth="20.0" text="" sortable="false"/>
<TableColumn fx:id="networkNodesAliasColumn" minWidth="80.0" prefWidth="180.0" maxWidth="300.0" text="Alias"/>
<TableColumn fx:id="networkNodesIdColumn" text="Node Id"/>
<TableColumn fx:id="networkNodesIPColumn" minWidth="150.0" prefWidth="250.0" maxWidth="300.0" text="IP"/>
</columns>
</TableView>
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true" preserveRatio="true">
<image><Image url="@../commons/images/eclair-shape.png"/></image>
</ImageView>
<Label fx:id="labelNodeId" text="N/A"/>
</children>
</VBox>
</content>
</Tab>
<Tab text="All Channels" fx:id="networkChannelsTab" closable="false">
<content>
<VBox spacing="10.0" styleClass="grid">
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="80.0">
<children>
<TableView fx:id="networkChannelsTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="networkChannelsIdColumn" minWidth="120.0" prefWidth="170.0" maxWidth="300.0" text="Channel Id"/>
<TableColumn fx:id="networkChannelsNode1Column" text="Node 1"/>
<TableColumn fx:id="networkChannelsNode2Column" text="Node 2"/>
</columns>
</TableView>
<Separator orientation="VERTICAL"/>
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent" />
<Label fx:id="labelAlias" text="N/A"/>
</children>
</VBox>
</content>
</Tab>
<Tab text="Activity" closable="false">
<content>
<AnchorPane>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<TabPane AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" AnchorPane.bottomAnchor="0.0"
styleClass="activities-tab" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab fx:id="paymentSentTab" closable="false" text="Sent">
<TableView fx:id="paymentSentTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentSentDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentSentAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentFeesColumn" text="Fees Paid (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentSentHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentReceivedTab" closable="false" text="Received">
<TableView fx:id="paymentReceivedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentReceivedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentReceivedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentReceivedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
<Tab fx:id="paymentRelayedTab" closable="false" text="Relayed">
<TableView fx:id="paymentRelayedTable" minHeight="50.0" prefHeight="5000.0">
<columnResizePolicy><TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/></columnResizePolicy>
<columns>
<TableColumn fx:id="paymentRelayedDateColumn" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0" text="Date"/>
<TableColumn fx:id="paymentRelayedAmountColumn" text="Amount (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedFeesColumn" text="Fees Earned (msat)"
styleClass="align-right" resizable="false" minWidth="150.0" prefWidth="150.0" maxWidth="150.0"/>
<TableColumn fx:id="paymentRelayedHashColumn" text="Payment Hash"/>
</columns>
</TableView>
</Tab>
</tabs>
</TabPane>
<Label AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" textAlignment="RIGHT"
maxWidth="180.0" wrapText="true" styleClass="activity-disclaimer"
text="Payment history will be erased when the node is shutdown." />
<Separator orientation="VERTICAL"/>
<Label text="HTTP" styleClass="badge, badge-http"/>
<Label fx:id="labelApi" styleClass="value" text="N/A"/>
</children>
</AnchorPane>
</content>
</Tab>
</tabs>
</TabPane>
</center>
<bottom>
<HBox styleClass="status-bar" spacing="10">
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="TCP" styleClass="badge, badge-tcp"/>
<Label fx:id="labelServer" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
<children>
<Separator orientation="VERTICAL"/>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="195.0">
<children>
<Label text="Bitcoin-core" textAlignment="RIGHT" textOverrun="CLIP"/>
<Label fx:id="bitcoinVersion" text="N/A" textOverrun="CLIP"/>
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP"/>
</children>
</HBox>
</children>
</HBox>
</bottom>
<top>
<MenuBar BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="Channels">
<items>
<MenuItem fx:id="menuOpen" mnemonicParsing="false" onAction="#handleOpenChannel"
text="Open channel..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem fx:id="menuSend" mnemonicParsing="false" onAction="#handleSendPayment"
text="Send Payment..."/>
<MenuItem fx:id="menuReceive" mnemonicParsing="false" onAction="#handleReceivePayment"
text="Receive Payment..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" onAction="#handleCloseRequest" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Tools">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleExportDot" text="Export Graph to .dot"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleOpenAbout" text="About Eclair..."/>
</items>
</Menu>
</menus>
</MenuBar>
</top>
</BorderPane>
<StackPane fx:id="blocker" styleClass="blocker-cover" opacity="0" visible="false" alignment="CENTER"
AnchorPane.topAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.rightAnchor="0">
<children>
<HBox alignment="CENTER_LEFT" HBox.hgrow="ALWAYS" onContextMenuRequested="#openNodeIdContext">
<HBox fx:id="blockerDialog" opacity="0" styleClass="blocker-dialog" fillHeight="false" alignment="CENTER_LEFT" spacing="20"
minWidth="430.0" minHeight="100.0" prefWidth="430.0" prefHeight="100.0" maxWidth="430.0" maxHeight="100.0">
<children>
<ImageView fitHeight="16.0" fitWidth="27.0" opacity="0.52" pickOnBounds="true" preserveRatio="true">
<image><Image url="@../commons/images/eclair-shape.png"/></image>
<ImageView fitHeight="40.0" fitWidth="40.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../commons/images/connection_icon.png" />
</image>
</ImageView>
<Label fx:id="labelNodeId" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="SOMETIMES" minWidth="80.0">
<children>
<Separator orientation="VERTICAL"/>
<Rectangle fx:id="rectRGB" width="7" height="7" fill="transparent" />
<Label fx:id="labelAlias" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="HTTP" styleClass="badge, badge-http"/>
<Label fx:id="labelApi" styleClass="value" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="85.0">
<children>
<Separator orientation="VERTICAL"/>
<Label text="TCP" styleClass="badge, badge-tcp"/>
<Label fx:id="labelServer" text="N/A"/>
</children>
</HBox>
<HBox alignment="CENTER_LEFT" HBox.hgrow="NEVER" minWidth="6.0">
<children>
<Separator orientation="VERTICAL"/>
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" HBox.hgrow="SOMETIMES" minWidth="195.0">
<children>
<Label text="Bitcoin-core" textAlignment="RIGHT" textOverrun="CLIP"/>
<Label fx:id="bitcoinVersion" text="N/A" textOverrun="CLIP"/>
<Label fx:id="bitcoinChain" styleClass="chain" text="(N/A)" textOverrun="CLIP"/>
<VBox spacing="10.0" GridPane.columnIndex="1">
<children>
<Label fx:id="message" styleClass="text-strong" text="Lost connection to Bitcoin Core..." wrapText="true" />
<Label fx:id="details" text="Make sure that Bitcoin core is up and running." wrapText="true" />
</children>
</VBox>
</children>
</HBox>
</children>
</HBox>
</bottom>
</StackPane>
</children>
<stylesheets>
<URL value="@main.css"/>
<URL value="@../commons/globals.css"/>
</stylesheets>
<top>
<MenuBar BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="Channels">
<items>
<MenuItem fx:id="menuOpen" mnemonicParsing="false" onAction="#handleOpenChannel"
text="Open channel..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem fx:id="menuSend" mnemonicParsing="false" onAction="#handleSendPayment"
text="Send Payment..."/>
<MenuItem fx:id="menuReceive" mnemonicParsing="false" onAction="#handleReceivePayment"
text="Receive Payment..."/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" onAction="#handleCloseRequest" text="Close"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Tools">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleExportDot" text="Export Graph to .dot"/>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<items>
<MenuItem mnemonicParsing="false" onAction="#handleOpenAbout" text="About Eclair..."/>
</items>
</Menu>
</menus>
</MenuBar>
</top>
</BorderPane>
</AnchorPane>

View File

@ -2,7 +2,6 @@ package fr.acinq.eclair
import java.io.File
import java.net.InetSocketAddress
import javafx.application.Platform
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.http.scaladsl.Http
@ -16,7 +15,7 @@ import com.sun.javafx.application.LauncherImpl
import fr.acinq.bitcoin.{Base58Check, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Script}
import fr.acinq.eclair.api.Service
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.blockchain.zmq.ZeroMQClient
import fr.acinq.eclair.blockchain.zmq.ZMQActor
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PeerWatcher}
import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.gui.{FxApp, FxPreloader}
@ -30,6 +29,7 @@ import org.slf4j.LoggerFactory
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Promise}
import scala.util.Try
case class CmdLineConfig(datadir: File = new File(System.getProperty("user.home"), ".eclair"), headless: Boolean = false)
@ -45,11 +45,16 @@ object Boot extends App with Logging {
opt[Unit]("headless").optional().action((_, c) => c.copy(headless = true)).text("runs eclair without a gui")
}
parser.parse(args, CmdLineConfig()) match {
case Some(config) if config.headless =>
case Some(config) if config.headless => try {
val s = new Setup(config.datadir.getAbsolutePath)
s.boostrap
} catch {
case t: Throwable =>
logger.error(s"fatal error: ${t.getMessage}")
System.exit(1)
}
case Some(config) => LauncherImpl.launchApplication(classOf[FxApp], classOf[FxPreloader], Array(config.datadir.getAbsolutePath))
case None => Platform.exit()
case None => System.exit(0)
}
}
@ -99,7 +104,8 @@ class Setup(datadir: String, actorSystemName: String = "default") extends Loggin
//val finalScriptPubKey = OP_0 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: Nil
val finalScriptPubKey = Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
val zmq = new ZeroMQClient(config.getString("bitcoind.zmq"), system.eventStream)
val zmqConnected = Promise[Boolean]()
val zmq = system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
val watcher = system.actorOf(SimpleSupervisor.props(PeerWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume))
val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => Props[LocalPaymentHandler]
@ -110,9 +116,8 @@ class Setup(datadir: String, actorSystemName: String = "default") extends Loggin
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, finalScriptPubKey), "switchboard", SupervisorStrategy.Resume))
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart))
val bound = Promise[Unit]()
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(bound)), "server", SupervisorStrategy.Restart))
Await.result(bound.future.recover { case _ => throw new TCPBindException(config.getInt("server.port")) }, 10 seconds)
val tcpBound = Promise[Unit]()
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
val _setup = this
val api = new Service {
@ -123,7 +128,11 @@ class Setup(datadir: String, actorSystemName: String = "default") extends Loggin
override val paymentInitiator: ActorRef = _setup.paymentInitiator
override val system: ActorSystem = _setup.system
}
Await.result(Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port")).recover { case _ => throw new TCPBindException(config.getInt("api.port")) }, 10 seconds)
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port"))
Try(Await.result(zmqConnected.future, 5 seconds)).recover { case _ => throw ZMQConnectionTimeoutException }.get
Try(Await.result(tcpBound.future, 5 seconds)).recover { case _ => throw new TCPBindException(config.getInt("server.port")) }.get
Try(Await.result(httpBound, 5 seconds)).recover { case _ => throw new TCPBindException(config.getInt("api.port")) }.get
val tasks = new Thread(new Runnable() {
override def run(): Unit = {
@ -156,3 +165,5 @@ object LogSetup {
}
case class TCPBindException(port: Int) extends RuntimeException
case object ZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")

View File

@ -2,6 +2,8 @@ package fr.acinq.eclair
import akka.actor.{Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy}
import scala.concurrent.duration._
/**
* This supervisor will supervise a single child actor using the provided SupervisorStrategy
* All incoming messages will be forwarded to the child actor.
@ -16,7 +18,8 @@ class SimpleSupervisor(childProps: Props, childName: String, strategy: Superviso
case msg => child forward msg
}
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => strategy }
// we allow at most <maxNrOfRetries> within <withinTimeRange>, otherwise the child actor is not restarted (this avoids restart loops)
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true, maxNrOfRetries = 100, withinTimeRange = 1 minute) { case _ => strategy }
}
object SimpleSupervisor {

View File

@ -0,0 +1,77 @@
package fr.acinq.eclair.blockchain.zmq
import akka.actor.{Actor, ActorLogging}
import fr.acinq.bitcoin.{Block, Transaction}
import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction}
import org.zeromq.ZMQ.Event
import org.zeromq.{ZContext, ZMQ, ZMsg}
import scala.concurrent.Promise
import scala.concurrent.duration._
import scala.util.Try
/**
* Created by PM on 04/04/2017.
*/
class ZMQActor(address: String, connected: Option[Promise[Boolean]] = None) extends Actor with ActorLogging {
val ctx = new ZContext
val subscriber = ctx.createSocket(ZMQ.SUB)
subscriber.monitor("inproc://events", ZMQ.EVENT_CONNECTED | ZMQ.EVENT_DISCONNECTED)
subscriber.connect(address)
subscriber.subscribe("rawblock".getBytes(ZMQ.CHARSET))
subscriber.subscribe("rawtx".getBytes(ZMQ.CHARSET))
val monitor = ctx.createSocket(ZMQ.PAIR)
monitor.connect("inproc://events")
import scala.concurrent.ExecutionContext.Implicits.global
// we check messages in a non-blocking manner with an interval, making sure to retrieve all messages before waiting again
def checkEvent: Unit = Option(Event.recv(monitor, ZMQ.DONTWAIT)) match {
case Some(event) =>
self ! event
checkEvent
case None =>
context.system.scheduler.scheduleOnce(1 second)(checkEvent)
}
def checkMsg: Unit = Option(ZMsg.recvMsg(subscriber, ZMQ.DONTWAIT)) match {
case Some(msg) =>
self ! msg
checkMsg
case None =>
context.system.scheduler.scheduleOnce(1 second)(checkMsg)
}
checkEvent
checkMsg
override def receive: Receive = {
case event: Event => event.getEvent match {
case ZMQ.EVENT_CONNECTED =>
log.info(s"connected to ${event.getAddress}")
Try(connected.map(_.success(true)))
context.system.eventStream.publish(ZMQConnected)
case ZMQ.EVENT_DISCONNECTED =>
log.warning(s"disconnected from ${event.getAddress}")
context.system.eventStream.publish(ZMQDisconnected)
case x => log.error(s"unexpected event $x")
}
case msg: ZMsg => msg.popString() match {
case "rawblock" =>
val block = Block.read(msg.pop().getData)
log.debug(s"received blockid=${block.blockId}")
context.system.eventStream.publish(NewBlock(block))
case "rawtx" =>
val tx = Transaction.read(msg.pop().getData)
log.debug(s"received txid=${tx.txid}")
context.system.eventStream.publish(NewTransaction(tx))
case topic => log.warning(s"unexpected topic=$topic")
}
}
}

View File

@ -1,43 +0,0 @@
package fr.acinq.eclair.blockchain.zmq
import akka.event.EventStream
import fr.acinq.bitcoin.{Block, Transaction}
import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction}
import grizzled.slf4j.Logging
import org.zeromq.ZMsg
/**
* Created by PM on 30/03/2017.
*/
class ZeroMQClient(addr: String, eventStream: EventStream) extends Logging {
new Thread(new Runnable {
override def run(): Unit = {
import org.zeromq.ZMQ
val context = ZMQ.context(1)
// Connect to weather server
val subscriber = context.socket(ZMQ.SUB)
subscriber.connect(addr)
//subscriber.subscribe("hashblock".getBytes(ZMQ.CHARSET))
//subscriber.subscribe("hashtx".getBytes(ZMQ.CHARSET))
subscriber.subscribe("rawblock".getBytes(ZMQ.CHARSET))
subscriber.subscribe("rawtx".getBytes(ZMQ.CHARSET))
while (true) {
val msg = ZMsg.recvMsg(subscriber)
msg.popString() match {
case "rawblock" =>
val block = Block.read(msg.pop().getData)
logger.debug(s"received blockid=${block.blockId}")
eventStream.publish(NewBlock(block))
case "rawtx" =>
val tx = Transaction.read(msg.pop().getData)
logger.debug(s"received txid=${tx.txid}")
eventStream.publish(NewTransaction(tx))
case _ => {}
}
}
subscriber.close()
}
}).start()
}

View File

@ -0,0 +1,10 @@
package fr.acinq.eclair.blockchain.zmq
/**
* Created by PM on 04/04/2017.
*/
sealed trait ZMQEvents
case object ZMQConnected extends ZMQEvents
case object ZMQDisconnected extends ZMQEvents

View File

@ -11,11 +11,12 @@ import javafx.stage.{Popup, Screen, Stage, WindowEvent}
import akka.actor.{Props, SupervisorStrategy}
import akka.stream.StreamTcpException
import fr.acinq.eclair.blockchain.zmq.ZMQEvents
import fr.acinq.eclair.channel.ChannelEvent
import fr.acinq.eclair.gui.controllers.{MainController, NotificationsController}
import fr.acinq.eclair.payment.PaymentEvent
import fr.acinq.eclair.router.NetworkEvent
import fr.acinq.eclair.{Setup, SimpleSupervisor, TCPBindException}
import fr.acinq.eclair.{Setup, SimpleSupervisor, TCPBindException, ZMQConnectionTimeoutException}
import grizzled.slf4j.Logging
@ -40,10 +41,11 @@ class FxApp extends Application with Logging {
val setup = new Setup(datadir)
val handlers = new Handlers(setup)
val controller = new MainController(handlers, setup, getHostServices)
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller, setup), "gui-updater", SupervisorStrategy.Resume))
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvents])
Platform.runLater(new Runnable {
override def run(): Unit = {
@ -74,8 +76,11 @@ class FxApp extends Application with Logging {
case TCPBindException(port) =>
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
case _: ConnectException | _: StreamTcpException =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin-core.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Please check that Bitcoin-core is started and that the RPC user, password and port are correct."))
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct."))
case ZMQConnectionTimeoutException =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct."))
case t: Throwable =>
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
}

View File

@ -9,14 +9,13 @@ import javafx.scene.layout.VBox
import akka.actor.{Actor, ActorLogging, ActorRef, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair.Setup
import fr.acinq.eclair.blockchain.zmq.{ZMQConnected, ZMQDisconnected}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.gui.controllers.{ChannelPaneController, MainController}
import fr.acinq.eclair.io.Reconnect
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
import fr.acinq.eclair.router.{ChannelDiscovered, ChannelLost, NodeDiscovered, NodeLost}
import fr.acinq.eclair.wire.{ChannelAnnouncement, NodeAnnouncement}
import org.jgrapht.graph.{DefaultEdge, SimpleGraph}
import scala.collection.JavaConversions._
@ -24,14 +23,7 @@ import scala.collection.JavaConversions._
/**
* Created by PM on 16/08/2016.
*/
class GUIUpdater(mainController: MainController, setup: Setup) extends Actor with ActorLogging {
class NamedEdge(val id: BinaryData) extends DefaultEdge {
override def toString: String = s"${id.toString.take(8)}..."
}
val graph = new SimpleGraph[BinaryData, NamedEdge](classOf[NamedEdge])
graph.addVertex(setup.nodeParams.privateKey.publicKey)
class GUIUpdater(mainController: MainController) extends Actor with ActorLogging {
def receive: Receive = main(Map())
@ -153,5 +145,13 @@ class GUIUpdater(mainController: MainController, setup: Setup) extends Actor wit
case p: PaymentRelayed =>
log.debug(s"payment relayed with h=${p.paymentHash}, amount=${p.amount}, feesEarned=${p.feesEarned}")
mainController.paymentRelayedList.prepend(p)
case ZMQConnected =>
log.debug("ZMQ connection online")
mainController.hideBlockerModal
case ZMQDisconnected =>
log.debug("ZMQ connection lost")
mainController.showBlockerModal
}
}

View File

@ -27,7 +27,7 @@ class Handlers(setup: Setup) extends Logging {
import setup._
private var notifsController:Option[NotificationsController] = None
private var notifsController: Option[NotificationsController] = None
def initNotifications (controller: NotificationsController) = {
notifsController = Option(controller)

View File

@ -4,6 +4,7 @@ import java.text.NumberFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import javafx.animation.{FadeTransition, ParallelTransition, SequentialTransition, TranslateTransition}
import javafx.application.{HostServices, Platform}
import javafx.beans.property._
import javafx.beans.value.{ChangeListener, ObservableValue}
@ -14,12 +15,12 @@ import javafx.fxml.FXML
import javafx.scene.control.TableColumn.CellDataFeatures
import javafx.scene.control._
import javafx.scene.input.ContextMenuEvent
import javafx.scene.layout.{BorderPane, VBox}
import javafx.scene.layout.{AnchorPane, HBox, StackPane, VBox}
import javafx.scene.paint.Color
import javafx.scene.shape.Rectangle
import javafx.stage.FileChooser.ExtensionFilter
import javafx.stage._
import javafx.util.Callback
import javafx.util.{Callback, Duration}
import fr.acinq.eclair.Setup
import fr.acinq.eclair.gui.Handlers
@ -34,7 +35,7 @@ import grizzled.slf4j.Logging
*/
class MainController(val handlers: Handlers, val setup: Setup, val hostServices: HostServices) extends Logging {
@FXML var root: BorderPane = _
@FXML var root: AnchorPane = _
var contextMenu: ContextMenu = _
// menu
@ -99,6 +100,9 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
@FXML var paymentRelayedHashColumn: TableColumn[PaymentRelayed, String] = _
@FXML var paymentRelayedDateColumn: TableColumn[PaymentRelayed, String] = _
@FXML var blocker: StackPane = _
@FXML var blockerDialog: HBox = _
val PAYMENT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val moneyFormatter = NumberFormat.getInstance(Locale.getDefault)
@ -358,36 +362,67 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
val fileChooser = new FileChooser
fileChooser.setTitle("Save as")
fileChooser.getExtensionFilters.addAll(new ExtensionFilter("DOT File (*.dot)", "*.dot"))
val file = fileChooser.showSaveDialog(root.getScene.getWindow)
val file = fileChooser.showSaveDialog(getWindow.getOrElse(null))
if (file != null) handlers.exportToDot(file)
}
@FXML def handleOpenChannel = {
val openChannelStage = new OpenChannelStage(handlers, setup)
openChannelStage.initOwner(root.getScene.getWindow)
openChannelStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(openChannelStage)
openChannelStage.show
}
@FXML def handleSendPayment = {
val sendPaymentStage = new SendPaymentStage(handlers, setup)
sendPaymentStage.initOwner(root.getScene.getWindow)
sendPaymentStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(sendPaymentStage)
sendPaymentStage.show
}
@FXML def handleReceivePayment = {
val receiveStage = new ReceivePaymentStage(handlers, setup)
receiveStage.initOwner(root.getScene.getWindow)
receiveStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(receiveStage)
receiveStage.show
}
@FXML def handleCloseRequest = root.getScene.getWindow.fireEvent(new WindowEvent(root.getScene.getWindow, WindowEvent.WINDOW_CLOSE_REQUEST))
def showBlockerModal = {
val fadeTransition = new FadeTransition(Duration.millis(300))
fadeTransition.setFromValue(0)
fadeTransition.setToValue(1)
val translateTransition = new TranslateTransition(Duration.millis(300))
translateTransition.setFromY(20)
translateTransition.setToY(0)
blocker.setVisible(true)
val ftCover = new FadeTransition(Duration.millis(200), blocker)
ftCover.setFromValue(0)
ftCover.setToValue(1)
ftCover.play
val t = new ParallelTransition(blockerDialog, fadeTransition, translateTransition)
t.setDelay(Duration.millis(200))
t.play
}
def hideBlockerModal = {
val ftCover = new FadeTransition(Duration.millis(400))
ftCover.setFromValue(1)
ftCover.setToValue(0)
val s = new SequentialTransition(blocker, ftCover)
s.setOnFinished(new EventHandler[ActionEvent]() {
override def handle(event: ActionEvent): Unit = blocker.setVisible(false)
})
s.play
}
private def getWindow: Option[Window] = {
Option(root).map(_.getScene.getWindow)
}
@FXML def handleCloseRequest = getWindow.map(_.fireEvent(new WindowEvent(getWindow.get, WindowEvent.WINDOW_CLOSE_REQUEST)))
@FXML def handleOpenAbout = {
val aboutStage = new AboutStage(hostServices)
aboutStage.initOwner(root.getScene.getWindow)
aboutStage.initOwner(getWindow.getOrElse(null))
positionAtCenter(aboutStage)
aboutStage.show
}
@ -395,7 +430,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
@FXML def openNodeIdContext(event: ContextMenuEvent) = contextMenu.show(labelNodeId, event.getScreenX, event.getScreenY)
def positionAtCenter(childStage: Stage) = {
childStage.setX(root.getScene.getWindow.getX + root.getScene.getWindow.getWidth / 2 - childStage.getWidth / 2)
childStage.setY(root.getScene.getWindow.getY + root.getScene.getWindow.getHeight / 2 - childStage.getHeight / 2)
childStage.setX(getWindow.map(w => w.getX + w.getWidth / 2 - childStage.getWidth / 2).getOrElse(0))
childStage.setY(getWindow.map(w => w.getY + w.getHeight / 2 - childStage.getHeight / 2).getOrElse(0))
}
}