Merge remote-tracking branch 'open/master' into os-merge-d5b5825

# Conflicts:
#	CONTRIBUTORS.md
#	tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
This commit is contained in:
Shams Asari 2018-06-28 15:10:58 +01:00
commit 07995e0eb0
19 changed files with 460 additions and 320 deletions

View File

@ -50,7 +50,7 @@ data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val mess
/**
* This model exposes raw event streams to and from the node.
*/
class NodeMonitorModel {
class NodeMonitorModel : AutoCloseable {
private val retryableStateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
private val stateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
@ -59,6 +59,7 @@ class NodeMonitorModel {
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
private val networkMapSubject = PublishSubject.create<MapChange>()
private var rpcConnection: CordaRPCConnection? = null
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
val vaultUpdates: Observable<Vault.Update<ContractState>> = vaultUpdatesSubject
@ -94,6 +95,17 @@ class NodeMonitorModel {
*/
class CordaRPCOpsWrapper(val cordaRPCOps: CordaRPCOps)
/**
* Disconnects from the Corda node for a clean client shutdown.
*/
override fun close() {
try {
rpcConnection?.notifyServerAndClose()
} catch (e: Exception) {
logger.error("Error closing RPC connection to node", e)
}
}
/**
* Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism
@ -155,9 +167,10 @@ class NodeMonitorModel {
}
private fun performRpcReconnect(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, shouldRetry: Boolean): List<StateMachineInfo> {
val connection = establishConnectionWithRetry(nodeHostAndPort, username, password, shouldRetry)
val proxy = connection.proxy
val proxy = establishConnectionWithRetry(nodeHostAndPort, username, password, shouldRetry).let { connection ->
rpcConnection = connection
connection.proxy
}
val (stateMachineInfos, stateMachineUpdatesRaw) = proxy.stateMachinesFeed()
@ -172,7 +185,7 @@ class NodeMonitorModel {
// It is good idea to close connection to properly mark the end of it. During re-connect we will create a new
// client and a new connection, so no going back to this one. Also the server might be down, so we are
// force closing the connection to avoid propagation of notification to the server side.
connection.forceClose()
rpcConnection?.forceClose()
// Perform re-connect.
performRpcReconnect(nodeHostAndPort, username, password, shouldRetry = true)
})
@ -185,18 +198,17 @@ class NodeMonitorModel {
}
private fun establishConnectionWithRetry(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, shouldRetry: Boolean): CordaRPCConnection {
val retryInterval = 5.seconds
val client = CordaRPCClient(
nodeHostAndPort,
CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = retryInterval
)
)
do {
val connection = try {
logger.info("Connecting to: $nodeHostAndPort")
val client = CordaRPCClient(
nodeHostAndPort,
CordaRPCClientConfiguration.DEFAULT.copy(
connectionMaxRetryInterval = retryInterval
)
)
val _connection = client.start(username, password)
// Check connection is truly operational before returning it.
val nodeInfo = _connection.proxy.nodeInfo()
@ -205,7 +217,7 @@ class NodeMonitorModel {
} catch (throwable: Throwable) {
if (shouldRetry) {
// Deliberately not logging full stack trace as it will be full of internal stacktraces.
logger.info("Exception upon establishing connection: " + throwable.message)
logger.info("Exception upon establishing connection: {}", throwable.message)
null
} else {
throw throwable

View File

@ -3,4 +3,3 @@ keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10002"
rpcAddress : "localhost:10003"
webAddress : "localhost:10004"

View File

@ -3,4 +3,3 @@ keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10005"
rpcAddress : "localhost:10006"
webAddress : "localhost:10007"

View File

@ -2,7 +2,6 @@ myLegalName : "O=Notary Service,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10000"
webAddress : "localhost:10001"
notary : {
validating : true
}

View File

@ -38,7 +38,6 @@ handling, and ensures the Corda service is run at boot.
basedir : "/opt/corda"
p2pAddress : "example.com:10002"
rpcAddress : "example.com:10003"
webAddress : "0.0.0.0:10004"
h2port : 11000
emailAddress : "you@example.com"
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"
@ -206,7 +205,6 @@ at boot, and means the Corda service stays running with no users connected to th
basedir : "C:\\Corda"
p2pAddress : "example.com:10002"
rpcAddress : "example.com:10003"
webAddress : "0.0.0.0:10004"
h2port : 11000
emailAddress: "you@example.com"
myLegalName : "O=Bank of Breakfast Tea, L=London, C=GB"

View File

@ -42,8 +42,6 @@ The most important fields regarding network configuration are:
is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the
resolvable name of a machine in a VPN.
* ``rpcAddress``: The address to which Artemis will bind for RPC calls.
* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress``
and ``rpcAddress`` if they are on the same machine.
* ``notary.serviceLegalName``: The name of the notary service, required to setup distributed notaries with the network-bootstrapper.
Starting the nodes
@ -55,8 +53,6 @@ be found in :doc:`network-bootstrapper`.
Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually
``Node started up and registered``, indicating that the node is fully started.
.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound.
In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends.
Logging

View File

@ -20,6 +20,7 @@ import javafx.stage.Stage
import jfxtras.resources.JFXtrasFontRoboto
import joptsimple.OptionParser
import net.corda.client.jfx.model.Models
import net.corda.client.jfx.model.NodeMonitorModel
import net.corda.client.jfx.model.observableValue
import net.corda.core.utilities.contextLogger
import net.corda.explorer.model.CordaViewModel
@ -32,6 +33,7 @@ import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
import tornadofx.addStageIcon
import tornadofx.find
import kotlin.system.exitProcess
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
@ -45,6 +47,8 @@ class Main : App(MainView::class) {
}
override fun start(stage: Stage) {
var nodeModel: NodeMonitorModel? = null
// Login to Corda node
super.start(stage)
stage.minHeight = 600.0
@ -54,27 +58,29 @@ class Main : App(MainView::class) {
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
initOwner(stage.scene.window)
}.showAndWait().get()
if (button != ButtonType.OK) it.consume()
if (button == ButtonType.OK) {
nodeModel?.close()
} else {
it.consume()
}
}
val hostname = parameters.named["host"]
val port = asInteger(parameters.named["port"])
val username = parameters.named["username"]
val password = parameters.named["password"]
var isLoggedIn = false
if ((hostname != null) && (port != null) && (username != null) && (password != null)) {
try {
loginView.login(hostname, port, username, password)
isLoggedIn = true
nodeModel = loginView.login(hostname, port, username, password)
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(stage.scene.window) }.showAndWait()
}
}
if (!isLoggedIn) {
if (nodeModel == null) {
stage.hide()
loginView.login()
nodeModel = loginView.login()
}
addOptionalViews()
(find(primaryView) as MainView).initializeControls()
@ -106,7 +112,7 @@ class Main : App(MainView::class) {
runInFxApplicationThread {
// [showAndWait] need to be in the FX thread.
ExceptionDialog(throwable).showAndWait()
System.exit(1)
exitProcess(1)
}
}
// Do this first before creating the notification bar, so it can autosize itself properly.

View File

@ -37,11 +37,14 @@ class LoginView : View(WINDOW_TITLE) {
private val port by objectProperty(SettingsModel::portProperty)
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
fun login(host: String, port: Int, username: String, password: String) {
getModel<NodeMonitorModel>().register(NetworkHostAndPort(host, port), username, password)
fun login(host: String, port: Int, username: String, password: String): NodeMonitorModel {
return getModel<NodeMonitorModel>().apply {
register(NetworkHostAndPort(host, port), username, password)
}
}
fun login() {
tailrec fun login(): NodeMonitorModel? {
var nodeModel: NodeMonitorModel? = null
val status = Dialog<LoginStatus>().apply {
dialogPane = root
setResultConverter {
@ -49,7 +52,7 @@ class LoginView : View(WINDOW_TITLE) {
ButtonBar.ButtonData.OK_DONE -> try {
root.isDisable = true
// TODO : Run this async to avoid UI lockup.
login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text)
nodeModel = login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text)
if (!rememberMe.value) {
username.value = ""
host.value = ""
@ -74,12 +77,13 @@ class LoginView : View(WINDOW_TITLE) {
initOwner(root.scene.window)
}.showAndWait().get()
if (button == ButtonType.OK) {
nodeModel?.close()
exitProcess(0)
}
}
}
}.showAndWait().get()
if (status != LoginStatus.loggedIn) login()
return if (status == LoginStatus.loggedIn) nodeModel else login()
}
init {

View File

@ -31,7 +31,6 @@ apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
dependencies {
compile "com.microsoft.azure:azure:1.8.0"
compile "com.github.docker-java:docker-java:3.0.6"
@ -50,8 +49,8 @@ dependencies {
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
compile "no.tornado:tornadofx:$tornadofx_version"
// ControlsFX: Extra controls for JavaFX.
compile "org.controlsfx:controlsfx:$controlsfx_version"
}
shadowJar {

View File

@ -1,44 +1,34 @@
@file:JvmName("Main")
package net.corda.bootstrapper
import javafx.application.Application
import net.corda.bootstrapper.backends.Backend
import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
import net.corda.bootstrapper.cli.AzureParser
import net.corda.bootstrapper.cli.CliParser
import net.corda.bootstrapper.cli.CommandLineInterface
import net.corda.bootstrapper.cli.GuiSwitch
import net.corda.bootstrapper.gui.Gui
import net.corda.bootstrapper.serialization.SerializationEngine
import picocli.CommandLine
val baseArgs = CliParser()
fun main(args: Array<String>) {
SerializationEngine.init()
CommandLine(baseArgs).parse(*args)
val entryPointArgs = GuiSwitch();
CommandLine(entryPointArgs).parse(*args)
if (entryPointArgs.usageHelpRequested) {
CommandLine.usage(AzureParser(), System.out)
if (baseArgs.gui) {
Application.launch(Gui::class.java)
return
}
if (entryPointArgs.gui) {
Gui.main(args)
} else {
val baseArgs = CliParser()
CommandLine(baseArgs).parse(*args)
val argParser: CliParser = when (baseArgs.backendType) {
AZURE -> {
val azureArgs = AzureParser()
CommandLine(azureArgs).parse(*args)
azureArgs
}
Backend.BackendType.LOCAL_DOCKER -> baseArgs
val argParser: CliParser = when (baseArgs.backendType) {
AZURE -> {
val azureArgs = AzureParser()
CommandLine(azureArgs).parse(*args)
azureArgs
}
CommandLineInterface().run(argParser)
Backend.BackendType.LOCAL_DOCKER -> baseArgs
}
CommandLineInterface().run(argParser)
}

View File

@ -30,6 +30,10 @@ interface NetworkBuilder {
fun withBackendOptions(options: Map<String, String>): NetworkBuilder
fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>>
fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder
fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder
fun onNodeInstancesRequested(callback: (List<NodeInstanceRequest>) -> Unit): NetworkBuilder
}
private class NetworkBuilderImpl : NetworkBuilder {
@ -40,11 +44,18 @@ private class NetworkBuilderImpl : NetworkBuilder {
@Volatile
private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {}
@Volatile
private var onNodeBuildStartCallback: (FoundNode) -> Unit = {}
@Volatile
private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {}
@Volatile
private var onNodePushStartCallback: ((BuiltNode) -> Unit) = {}
@Volatile
private var onNodePushedCallback: ((PushedNode) -> Unit) = {}
@Volatile
private var onNodeInstanceRequestedCallback: (List<NodeInstanceRequest>) -> Unit = {}
@Volatile
private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {}
@Volatile
private var nodeCounts = mapOf<String, Int>()
@Volatile
@ -67,6 +78,12 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this
}
override fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder {
this.onNodeBuildStartCallback = callback
return this;
}
override fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder {
this.onNodeBuiltCallback = callback
return this
@ -77,6 +94,11 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this
}
override fun onNodeInstancesRequested(callback: (List<NodeInstanceRequest>) -> Unit): NetworkBuilder {
this.onNodeInstanceRequestedCallback = callback
return this
}
override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder {
this.onNodeInstanceCallback = callback;
return this
@ -107,6 +129,12 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this
}
override fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder {
this.onNodePushStartCallback = callback;
return this;
}
override fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>> {
val cacheDir = File(workingDir, cacheDirName)
val baseDir = workingDir!!
@ -132,6 +160,7 @@ private class NetworkBuilderImpl : NetworkBuilder {
val notaryDiscoveryFuture = CompletableFuture.supplyAsync {
val copiedNotaries = notaryFinder.findNotaries()
.map { foundNode: FoundNode ->
onNodeBuildStartCallback.invoke(foundNode)
notaryCopier.copyNotary(foundNode)
}
volume.notariesForNetworkParams(copiedNotaries)
@ -141,14 +170,15 @@ private class NetworkBuilderImpl : NetworkBuilder {
val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries ->
copiedNotaries
.map { copiedNotary ->
nodeBuilder.buildNode(copiedNotary)
nodeBuilder.buildNode(copiedNotary).also(onNodeBuiltCallback)
}.map { builtNotary ->
nodePusher.pushNode(builtNotary)
onNodePushStartCallback(builtNotary)
nodePusher.pushNode(builtNotary).thenApply { it.also(onNodePushedCallback) }
}.map { pushedNotary ->
pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it) }
pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it).also { onNodeInstanceRequestedCallback.invoke(listOf(it)) } }
}.map { instanceRequest ->
instanceRequest.thenComposeAsync { request ->
nodeInstantiator.instantiateNotaryInstance(request)
nodeInstantiator.instantiateNotaryInstance(request).thenApply { it.also(onNodeInstanceCallback) }
}
}.toSingleFuture()
}
@ -161,6 +191,7 @@ private class NetworkBuilderImpl : NetworkBuilder {
it
}
}.map { copiedNode: CopiedNode ->
onNodeBuildStartCallback.invoke(copiedNode)
nodeBuilder.buildNode(copiedNode).let {
onNodeBuiltCallback.invoke(it)
it
@ -172,7 +203,8 @@ private class NetworkBuilderImpl : NetworkBuilder {
}
}.map { pushedNode ->
pushedNode.thenApplyAsync {
nodeInstantiator.createInstanceRequests(it, nodeCount)
nodeInstantiator.createInstanceRequests(it, nodeCount).also(onNodeInstanceRequestedCallback)
}
}.map { instanceRequests ->
instanceRequests.thenComposeAsync { requests ->

View File

@ -22,8 +22,16 @@ interface Backend {
val instantiator: Instantiator
val volume: Volume
enum class BackendType {
AZURE, LOCAL_DOCKER
enum class BackendType(val displayName: String) {
AZURE("Azure Containers"), LOCAL_DOCKER("Local Docker");
override fun toString(): String {
return this.displayName
}
}
operator fun component1(): ContainerPusher {

View File

@ -17,7 +17,7 @@ class CommandLineInterface {
fun run(parsedArgs: CliParser) {
val baseDir = parsedArgs.baseDirectory
val cacheDir = File(baseDir, Constants.BOOTSTRAPPER_DIR_NAME)
val networkName = parsedArgs.name
val networkName = parsedArgs.name ?: "corda-network"
val objectMapper = Constants.getContextMapper()
val contextFile = File(cacheDir, "$networkName.yaml")
if (parsedArgs.isNew()) {

View File

@ -7,30 +7,20 @@ import picocli.CommandLine
import picocli.CommandLine.Option
import java.io.File
open class GuiSwitch {
open class CliParser {
@Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"])
var name: String? = null
@Option(names = ["-h", "--help"], usageHelp = true, description = ["display this help message"])
var usageHelpRequested: Boolean = false
@Option(names = ["-g", "--gui"], description = ["Run in Gui Mode"])
@Option(names = ["-g", "--gui"], description = ["Run the graphical user interface"])
var gui = false
@CommandLine.Unmatched
var unmatched = arrayListOf<String>()
}
open class CliParser : GuiSwitch() {
@Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"], required = true)
lateinit var name: String
@Option(names = ["-d", "--nodes-directory"], description = ["The directory to search for nodes in"])
var baseDirectory = File(System.getProperty("user.dir"))
@Option(names = ["-b", "--backend"], description = ["The backend to use when instantiating nodes"])
var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER
@Option(names = ["-nodes"], split = ":", description = ["The number of each node to create NodeX:2 will create two instances of NodeX"])
@Option(names = ["--nodes"], split = ":", description = ["The number of each node to create. NodeX:2 will create two instances of NodeX"])
var nodes: MutableMap<String, Int> = hashMapOf()
@Option(names = ["--add", "-a"])
@ -43,11 +33,9 @@ open class CliParser : GuiSwitch() {
open fun backendOptions(): Map<String, String> {
return emptyMap()
}
}
class AzureParser : CliParser() {
companion object {
val regions = Region.values().map { it.name() to it }.toMap()
}
@ -64,5 +52,4 @@ class AzureParser : CliParser() {
override fun backendOptions(): Map<String, String> {
return mapOf(Constants.REGION_ARG_NAME to region.name())
}
}

View File

@ -2,128 +2,143 @@ package net.corda.bootstrapper.gui
import com.microsoft.azure.management.resources.fluentcore.arm.Region
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.ObservableListBase
import javafx.collections.transformation.SortedList
import javafx.event.EventHandler
import javafx.scene.control.ChoiceDialog
import javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY
import javafx.scene.control.TextInputDialog
import javafx.fxml.FXML
import javafx.scene.control.*
import javafx.scene.input.MouseEvent
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.stage.DirectoryChooser
import net.corda.bootstrapper.Constants
import net.corda.bootstrapper.GuiUtils
import net.corda.bootstrapper.NetworkBuilder
import net.corda.bootstrapper.backends.Backend
import net.corda.bootstrapper.baseArgs
import net.corda.bootstrapper.context.Context
import net.corda.bootstrapper.nodes.*
import net.corda.bootstrapper.notaries.NotaryFinder
import org.apache.commons.lang3.RandomStringUtils
import org.controlsfx.control.SegmentedButton
import tornadofx.*
import java.io.File
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import kotlin.Comparator
import kotlin.collections.ArrayList
class BootstrapperView : View("Network Bootstrapper") {
class BootstrapperView : View("Corda Network Builder") {
val YAML_MAPPER = Constants.getContextMapper()
override val root: VBox by fxml("/views/mainPane.fxml")
val controller: State by inject()
val textarea = textarea {
maxWidth = Double.MAX_VALUE
maxHeight = Double.MAX_VALUE
}
val localDockerBtn: ToggleButton by fxid()
val azureBtn: ToggleButton by fxid()
val nodeTableView: TableView<NodeTemplateInfo> by fxid()
val templateChoiceBox: ChoiceBox<String> by fxid()
val buildButton: Button by fxid()
val addInstanceButton: Button by fxid()
val infoTextArea: TextArea by fxid()
override val root = vbox {
init {
visuallyTweakBackendSelector()
menubar {
menu("File") {
item("Open") {
action {
selectNodeDirectory().thenAcceptAsync({ (notaries: List<FoundNode>, nodes: List<FoundNode>) ->
controller.nodes(nodes)
controller.notaries(notaries)
})
buildButton.run {
enableWhen { controller.baseDir.isNotNull }
action {
var networkName = "corda-network"
val selectedBackEnd = when {
azureBtn.isSelected -> Backend.BackendType.AZURE
localDockerBtn.isSelected -> Backend.BackendType.LOCAL_DOCKER
else -> kotlin.error("Unknown backend selected")
}
val backendParams = when (selectedBackEnd) {
Backend.BackendType.LOCAL_DOCKER -> {
emptyMap()
}
Backend.BackendType.AZURE -> {
val pair = setupAzureRegionOptions()
networkName = pair.second
pair.first
}
}
item("Build") {
enableWhen(controller.baseDir.isNotNull)
action {
controller.clear()
val availableBackends = getAvailableBackends()
val backend = ChoiceDialog<Backend.BackendType>(availableBackends.first(), availableBackends).showAndWait()
var networkName = "gui-network"
backend.ifPresent { selectedBackEnd ->
val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
val result = NetworkBuilder.instance()
.withBasedir(controller.baseDir.get())
.withNetworkName(networkName)
.onNodeStartBuild(controller::onBuild)
.onNodeBuild(controller::addBuiltNode)
.onNodePushStart(controller::addBuiltNode)
.onNodePushed(controller::addPushedNode)
.onNodeInstancesRequested(controller::addInstanceRequests)
.onNodeInstance(controller::addInstance)
.withBackend(selectedBackEnd)
.withNodeCounts(nodeCount)
.withBackendOptions(backendParams)
.build()
val backendParams = when (selectedBackEnd) {
Backend.BackendType.LOCAL_DOCKER -> {
result.handle { v, t ->
runLater {
if (t != null) {
GuiUtils.showException("Failed to build network", "Failure due to", t)
} else {
controller.networkContext.set(v.second)
}
}
}
}
}
emptyMap<String, String>()
}
Backend.BackendType.AZURE -> {
val defaultName = RandomStringUtils.randomAlphabetic(4) + "-network"
val textInputDialog = TextInputDialog(defaultName)
textInputDialog.title = "Choose Network Name"
networkName = textInputDialog.showAndWait().orElseGet { defaultName }
mapOf(Constants.REGION_ARG_NAME to ChoiceDialog<Region>(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name())
}
templateChoiceBox.run {
enableWhen { controller.networkContext.isNotNull }
controller.networkContext.addListener { _, _, newValue ->
if (newValue != null) {
items = object : ObservableListBase<String>() {
override fun get(index: Int): String {
return controller.foundNodes[index].id
}
override val size: Int
get() = controller.foundNodes.size
}
selectionModel.select(controller.foundNodes[0].id)
}
}
}
addInstanceButton.run {
enableWhen { controller.networkContext.isNotNull }
action {
templateChoiceBox.selectionModel.selectedItem?.let { nodeToAdd ->
val context = controller.networkContext.value
runLater {
val (_, instantiator, _) = Backend.fromContext(
context,
File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
controller.addInstanceRequest(nodeToAdd)
nodeAdder.addNode(context, nodeToAdd).handleAsync { instanceInfo, t ->
t?.let {
GuiUtils.showException("Failed", "Failed to add node", it)
}
val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
val result = NetworkBuilder.instance()
.withBasedir(controller.baseDir.get())
.withNetworkName(networkName)
.onNodeBuild(controller::addBuiltNode)
.onNodePushed(controller::addPushedNode)
.onNodeInstance(controller::addInstance)
.withBackend(selectedBackEnd)
.withNodeCounts(nodeCount)
.withBackendOptions(backendParams)
.build()
result.handle { v, t ->
instanceInfo?.let {
runLater {
if (t != null) {
GuiUtils.showException("Failed to build network", "Failure due to", t)
} else {
controller.networkContext.set(v.second)
}
}
}
}
}
}
item("Add Node") {
enableWhen(controller.networkContext.isNotNull)
action {
val foundNodes = controller.foundNodes.map { it.id }
val nodeToAdd = ChoiceDialog<String>(foundNodes.first(), *foundNodes.toTypedArray()).showAndWait()
val context = controller.networkContext.value
nodeToAdd.ifPresent { node ->
runLater {
val (_, instantiator, _) = Backend.fromContext(
context,
File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
nodeAdder.addNode(context, node).handleAsync { instanceInfo, t ->
t?.let {
GuiUtils.showException("Failed", "Failed to add node", it)
}
instanceInfo?.let {
runLater {
controller.addInstance(NodeInstanceTableEntry(
it.groupId,
it.instanceName,
it.instanceAddress,
it.reachableAddress,
it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
it.portMapping[Constants.NODE_SSHD_PORT]
?: Constants.NODE_SSHD_PORT))
}
}
controller.addInstance(NodeInstanceEntry(
it.groupId,
it.instanceName,
it.instanceAddress,
it.reachableAddress,
it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
it.portMapping[Constants.NODE_SSHD_PORT]
?: Constants.NODE_SSHD_PORT))
}
}
}
@ -132,89 +147,72 @@ class BootstrapperView : View("Network Bootstrapper") {
}
}
hbox {
vbox {
label("Nodes to build")
val foundNodesTable = tableview(controller.foundNodes) {
readonlyColumn("ID", FoundNodeTableEntry::id)
column("Count", FoundNodeTableEntry::count).makeEditable()
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
foundNodesTable.columnResizePolicy = CONSTRAINED_RESIZE_POLICY
label("Notaries to build")
val notaryListView = listview(controller.foundNotaries) {
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
notaryListView.cellFormat { text = it.name }
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vbox {
label("Built Nodes")
tableview(controller.builtNodes) {
readonlyColumn("ID", BuiltNodeTableEntry::id)
readonlyColumn("LocalImageId", BuiltNodeTableEntry::localImageId)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
label("Pushed Nodes")
tableview(controller.pushedNodes) {
readonlyColumn("ID", PushedNode::name)
readonlyColumn("RemoteImageId", PushedNode::remoteImageName)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
borderpane {
top = vbox {
label("Instances")
tableview(controller.nodeInstances) {
onMouseClicked = EventHandler<MouseEvent> { _ ->
textarea.text = YAML_MAPPER.writeValueAsString(selectionModel.selectedItem)
}
readonlyColumn("ID", NodeInstanceTableEntry::id)
readonlyColumn("InstanceId", NodeInstanceTableEntry::nodeInstanceName)
readonlyColumn("Address", NodeInstanceTableEntry::address)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
}
}
center = textarea
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vgrow = Priority.ALWAYS
nodeTableView.run {
items = controller.sortedNodes
column("ID", NodeTemplateInfo::templateId)
column("Type", NodeTemplateInfo::nodeType)
column("Local Docker Image", NodeTemplateInfo::localDockerImageId)
column("Repository Image", NodeTemplateInfo::repositoryImageId)
column("Status", NodeTemplateInfo::status)
columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
hgrow = Priority.ALWAYS
onMouseClicked = EventHandler<MouseEvent> { _ ->
val selectedItem: NodeTemplateInfo = selectionModel.selectedItem ?: return@EventHandler
infoTextArea.text = YAML_MAPPER.writeValueAsString(translateForPrinting(selectedItem))
}
}
try {
processSelectedDirectory(baseArgs.baseDirectory)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getAvailableBackends(): List<Backend.BackendType> {
return Backend.BackendType.values().toMutableList();
private fun visuallyTweakBackendSelector() {
// The SegmentedButton will jam together the two toggle buttons in a way
// that looks more modern.
val hBox = localDockerBtn.parent as HBox
val idx = hBox.children.indexOf(localDockerBtn)
// Adding this to the hbox will re-parent the two toggle buttons into the
// SegmentedButton control, so we have to put it in the same position as
// the original buttons. Unfortunately it's not so Scene Builder friendly.
hBox.children.add(idx, SegmentedButton(localDockerBtn, azureBtn).apply {
styleClass.add(SegmentedButton.STYLE_CLASS_DARK)
})
}
fun selectNodeDirectory(): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> {
val fileChooser = DirectoryChooser();
fileChooser.initialDirectory = File(System.getProperty("user.home"))
val file = fileChooser.showDialog(null)
controller.baseDir.set(file)
return processSelectedDirectory(file)
private fun setupAzureRegionOptions(): Pair<Map<String, String>, String> {
var networkName1 = RandomStringUtils.randomAlphabetic(4) + "-network"
val textInputDialog = TextInputDialog(networkName1)
textInputDialog.title = "Azure Resource Group"
networkName1 = textInputDialog.showAndWait().orElseGet { networkName1 }
return Pair(mapOf(Constants.REGION_ARG_NAME to ChoiceDialog<Region>(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name()), networkName1)
}
private fun translateForPrinting(selectedItem: NodeTemplateInfo): Any {
return object {
val templateId = selectedItem.templateId.get()
val nodeType = selectedItem.nodeType.get()
val localDockerImageId = selectedItem.localDockerImageId.get()
val repositoryImageId = selectedItem.repositoryImageId.get()
val status = selectedItem.status.get()
val instances = selectedItem.instances.map { it }
}
}
fun processSelectedDirectory(dir: File): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> {
@FXML
fun onOpenClicked() {
val chooser = DirectoryChooser()
chooser.initialDirectory = File(System.getProperty("user.home"))
val file: File = chooser.showDialog(null) ?: return // Null means user cancelled.
processSelectedDirectory(file)
}
private fun processSelectedDirectory(dir: File) {
controller.clearAll()
controller.baseDir.set(dir)
val foundNodes = CompletableFuture.supplyAsync {
val nodeFinder = NodeFinder(dir)
nodeFinder.findNodes()
@ -223,101 +221,148 @@ class BootstrapperView : View("Network Bootstrapper") {
val notaryFinder = NotaryFinder(dir)
notaryFinder.findNotaries()
}
return foundNodes.thenCombine(foundNotaries) { nodes, notaries ->
foundNodes.thenCombine(foundNotaries) { nodes, notaries ->
notaries to nodes
}.thenAcceptAsync({ (notaries: List<FoundNode>, nodes: List<FoundNode>) ->
runLater {
controller.foundNodes(nodes)
controller.notaries(notaries)
}
})
}
class NodeTemplateInfo(templateId: String, type: NodeType) {
val templateId: SimpleStringProperty = object : SimpleStringProperty(templateId) {
override fun toString(): String {
return this.get()?.toString() ?: "null"
}
}
}
}
class State : Controller() {
val foundNodes = Collections.synchronizedList(ArrayList<FoundNodeTableEntry>()).observable()
val builtNodes = Collections.synchronizedList(ArrayList<BuiltNodeTableEntry>()).observable()
val pushedNodes = Collections.synchronizedList(ArrayList<PushedNode>()).observable()
private val backingUnsortedInstances = Collections.synchronizedList(ArrayList<NodeInstanceTableEntry>()).observable()
val nodeInstances = SortedList(backingUnsortedInstances, COMPARATOR)
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
val networkContext = SimpleObjectProperty<Context>(null)
fun clear() {
builtNodes.clear()
pushedNodes.clear()
backingUnsortedInstances.clear()
networkContext.set(null)
val nodeType: SimpleObjectProperty<NodeType> = SimpleObjectProperty(type)
val localDockerImageId: SimpleStringProperty = SimpleStringProperty()
val repositoryImageId: SimpleStringProperty = SimpleStringProperty()
val status: SimpleObjectProperty<NodeBuildStatus> = SimpleObjectProperty(NodeBuildStatus.DISCOVERED)
val instances: MutableList<NodeInstanceEntry> = ArrayList()
val numberOfInstancesWaiting: AtomicInteger = AtomicInteger(-1)
}
fun nodes(nodes: List<FoundNode>) {
foundNodes.clear()
nodes.forEach { addFoundNode(it) }
enum class NodeBuildStatus {
DISCOVERED, LOCALLY_BUILDING, LOCALLY_BUILT, REMOTE_PUSHING, REMOTE_PUSHED, INSTANTIATING, INSTANTIATED,
}
fun notaries(notaries: List<FoundNode>) {
foundNotaries.clear()
notaries.forEach { runLater { foundNotaries.add(it) } }
enum class NodeType {
NODE, NOTARY
}
var baseDir = SimpleObjectProperty<File>(null)
class State : Controller() {
val foundNodes = Collections.synchronizedList(ArrayList<FoundNodeTableEntry>()).observable()
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
val networkContext = SimpleObjectProperty<Context>(null)
val unsortedNodes = Collections.synchronizedList(ArrayList<NodeTemplateInfo>()).observable()
val sortedNodes = SortedList(unsortedNodes, Comparator<NodeTemplateInfo> { o1, o2 ->
compareValues(o1.nodeType.toString() + o1.templateId, o2.nodeType.toString() + o2.templateId) * -1
})
fun addFoundNode(foundNode: FoundNode) {
runLater {
foundNodes.add(FoundNodeTableEntry(foundNode.name))
fun clear() {
networkContext.set(null)
}
}
fun addBuiltNode(builtNode: BuiltNode) {
runLater {
builtNodes.add(BuiltNodeTableEntry(builtNode.name, builtNode.localImageId))
fun clearAll() {
networkContext.set(null)
foundNodes.clear()
foundNotaries.clear()
unsortedNodes.clear()
}
}
fun addPushedNode(pushedNode: PushedNode) {
runLater {
pushedNodes.add(pushedNode)
fun foundNodes(nodesToAdd: List<FoundNode>) {
foundNodes.clear()
nodesToAdd.forEach {
runLater {
foundNodes.add(FoundNodeTableEntry(it.name))
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NODE))
}
}
}
}
fun addInstance(nodeInstance: NodeInstance) {
runLater {
backingUnsortedInstances.add(NodeInstanceTableEntry(
fun notaries(notaries: List<FoundNode>) {
foundNotaries.clear()
notaries.forEach {
runLater {
foundNotaries.add(it)
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NOTARY))
}
}
}
var baseDir = SimpleObjectProperty<File>(null)
fun addBuiltNode(builtNode: BuiltNode) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == builtNode.name }
foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILT)
foundNode?.localDockerImageId?.set(builtNode.localImageId)
}
}
fun addPushedNode(pushedNode: PushedNode) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == pushedNode.name }
foundNode?.status?.set(NodeBuildStatus.REMOTE_PUSHED)
foundNode?.repositoryImageId?.set(pushedNode.remoteImageName)
}
}
fun onBuild(nodeBuilding: FoundNode) {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeBuilding.name }
foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILDING)
}
fun addInstance(nodeInstance: NodeInstance) {
addInstance(NodeInstanceEntry(
nodeInstance.name,
nodeInstance.nodeInstanceName,
nodeInstance.expectedFqName,
nodeInstance.reachableAddress,
nodeInstance.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT))
nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT)
)
}
}
fun addInstance(nodeInstance: NodeInstanceTableEntry) {
runLater {
backingUnsortedInstances.add(nodeInstance)
}
}
companion object {
val COMPARATOR: (NodeInstanceTableEntry, NodeInstanceTableEntry) -> Int = { o1, o2 ->
if (o1.id == (o2.id)) {
o1.nodeInstanceName.compareTo(o2.nodeInstanceName)
} else {
o1.id.compareTo(o2.id)
fun addInstanceRequests(requests: List<NodeInstanceRequest>) {
requests.firstOrNull()?.let { request ->
unsortedNodes.find { it.templateId.get() == request.name }?.let {
it.numberOfInstancesWaiting.set(requests.size)
it.status.set(NodeBuildStatus.INSTANTIATING)
}
}
}
fun addInstance(nodeInstance: NodeInstanceEntry) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeInstance.id }
foundNode?.instances?.add(nodeInstance)
if (foundNode != null && foundNode.instances.size == foundNode.numberOfInstancesWaiting.get()) {
foundNode.status.set(NodeBuildStatus.INSTANTIATED)
}
}
}
fun addInstanceRequest(nodeToAdd: String) {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeToAdd }
foundNode?.numberOfInstancesWaiting?.incrementAndGet()
foundNode?.status?.set(NodeBuildStatus.INSTANTIATING)
}
}
data class NodeInstanceEntry(val id: String,
val nodeInstanceName: String,
val address: String,
val locallyReachableAddress: String,
val rpcPort: Int,
val sshPort: Int)
}
data class FoundNodeTableEntry(val id: String,
@Volatile var count: Int = 1)
data class BuiltNodeTableEntry(val id: String, val localImageId: String)
data class NodeInstanceTableEntry(val id: String,
val nodeInstanceName: String,
val address: String,
val locallyReachableAddress: String,
val rpcPort: Int,
val sshPort: Int)
data class FoundNodeTableEntry(val id: String, @Volatile var count: Int = 1)

View File

@ -1,11 +1,11 @@
package net.corda.bootstrapper.gui
import javafx.application.Application
import tornadofx.App
import javafx.stage.Stage
import tornadofx.*
class Gui : App(BootstrapperView::class) {
companion object {
@JvmStatic
fun main(args: Array<String>) = Application.launch(Gui::class.java, *args)
override fun start(stage: Stage) {
super.start(stage)
stage.scene.stylesheets.add("/views/bootstrapper.css")
}
}

View File

@ -0,0 +1,13 @@
.top-pane {
-fx-background-color: white;
}
.top-pane > .button, .top-pane .toggle-button {
-fx-padding: 15px;
-fx-base: white;
}
.top-pane > .choice-box {
-fx-padding: 10px;
-fx-base: white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<VBox prefHeight="768.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
<children>
<HBox alignment="CENTER_LEFT" minHeight="-Infinity" prefHeight="75.0" prefWidth="1073.0" styleClass="top-pane">
<children>
<ImageView fitHeight="150.0" fitWidth="100.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@cordalogo.png" />
</image>
</ImageView>
<Pane prefHeight="75.0" prefWidth="21.0" />
<Button mnemonicParsing="false" onAction="#onOpenClicked" text="Open nodes ..." />
<Button fx:id="buildButton" mnemonicParsing="false" text="Build">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
<ToggleButton fx:id="localDockerBtn" mnemonicParsing="false" selected="true" text="Local Docker">
<toggleGroup>
<ToggleGroup fx:id="target" />
</toggleGroup>
</ToggleButton>
<ToggleButton fx:id="azureBtn" mnemonicParsing="false" text="Azure" toggleGroup="$target" />
<ChoiceBox fx:id="templateChoiceBox" prefWidth="150.0">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</ChoiceBox>
<Button fx:id="addInstanceButton" mnemonicParsing="false" text="Add Instance">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
</children>
</HBox>
<HBox maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="461.0" prefWidth="1014.0" VBox.vgrow="ALWAYS">
<children>
<SplitPane dividerPositions="0.6940371456500489" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<items>
<TableView fx:id="nodeTableView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="662.0" />
<TextArea fx:id="infoTextArea" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="353.0" />
</items>
</SplitPane>
</children>
</HBox>
</children>
</VBox>