diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 5d904fcc4a..555af2e2f8 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -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() private val stateMachineUpdatesSubject = PublishSubject.create() @@ -59,6 +59,7 @@ class NodeMonitorModel { private val stateMachineTransactionMappingSubject = PublishSubject.create() private val progressTrackingSubject = PublishSubject.create() private val networkMapSubject = PublishSubject.create() + private var rpcConnection: CordaRPCConnection? = null val stateMachineUpdates: Observable = stateMachineUpdatesSubject val vaultUpdates: Observable> = 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 { - - 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 diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 7547300ca6..efe2e546c2 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -3,4 +3,3 @@ keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10002" rpcAddress : "localhost:10003" -webAddress : "localhost:10004" diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index f922e91536..c1eb95ba71 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -3,4 +3,3 @@ keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10005" rpcAddress : "localhost:10006" -webAddress : "localhost:10007" diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 3f700628dc..0b6210a22d 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -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 } diff --git a/docs/source/deploying-a-node.rst b/docs/source/deploying-a-node.rst index f28488c6e3..83eea328aa 100644 --- a/docs/source/deploying-a-node.rst +++ b/docs/source/deploying-a-node.rst @@ -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" diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index b8946a2841..7b329272ea 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -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 diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt index c0c20fb65a..80a72c8695 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt @@ -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. diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt index 7e66cbfd1e..332dcc586f 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt @@ -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().register(NetworkHostAndPort(host, port), username, password) + fun login(host: String, port: Int, username: String, password: String): NodeMonitorModel { + return getModel().apply { + register(NetworkHostAndPort(host, port), username, password) + } } - fun login() { + tailrec fun login(): NodeMonitorModel? { + var nodeModel: NodeMonitorModel? = null val status = Dialog().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 { diff --git a/tools/network-bootstrapper/build.gradle b/tools/network-bootstrapper/build.gradle index 7cc76f0695..b49b5e87bc 100644 --- a/tools/network-bootstrapper/build.gradle +++ b/tools/network-bootstrapper/build.gradle @@ -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 { diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index 38a5539876..eb38bf11cb 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -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) { 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) } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt index f4f36d0381..6b4ee0220f 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/NetworkBuilder.kt @@ -30,6 +30,10 @@ interface NetworkBuilder { fun withBackendOptions(options: Map): NetworkBuilder fun build(): CompletableFuture, Context>> + fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder + fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder + fun onNodeInstancesRequested(callback: (List) -> 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) -> Unit = {} + @Volatile private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {} + @Volatile private var nodeCounts = mapOf() @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) -> 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, 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 -> diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt index 85209c1df5..2737411593 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/backends/Backend.kt @@ -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 { diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt index b7a27fe795..1836f5e76a 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandLineInterface.kt @@ -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()) { diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt index 164f6ec092..9eb7305bd5 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/cli/CommandParsers.kt @@ -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() -} - -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 = hashMapOf() @Option(names = ["--add", "-a"]) @@ -43,11 +33,9 @@ open class CliParser : GuiSwitch() { open fun backendOptions(): Map { 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 { return mapOf(Constants.REGION_ARG_NAME to region.name()) } - } \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt index 32be5574eb..d9eaee3e04 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/BootstrapperView.kt @@ -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 by fxid() + val templateChoiceBox: ChoiceBox 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, nodes: List) -> - 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(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() - } - 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.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() { + 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(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 { _ -> - 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 { _ -> + 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 { - 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, List>> { - 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, 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.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, List>> { + @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, nodes: List) -> + 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()).observable() - val builtNodes = Collections.synchronizedList(ArrayList()).observable() - val pushedNodes = Collections.synchronizedList(ArrayList()).observable() - - private val backingUnsortedInstances = Collections.synchronizedList(ArrayList()).observable() - val nodeInstances = SortedList(backingUnsortedInstances, COMPARATOR) - - val foundNotaries = Collections.synchronizedList(ArrayList()).observable() - val networkContext = SimpleObjectProperty(null) - - fun clear() { - builtNodes.clear() - pushedNodes.clear() - backingUnsortedInstances.clear() - networkContext.set(null) + val nodeType: SimpleObjectProperty = SimpleObjectProperty(type) + val localDockerImageId: SimpleStringProperty = SimpleStringProperty() + val repositoryImageId: SimpleStringProperty = SimpleStringProperty() + val status: SimpleObjectProperty = SimpleObjectProperty(NodeBuildStatus.DISCOVERED) + val instances: MutableList = ArrayList() + val numberOfInstancesWaiting: AtomicInteger = AtomicInteger(-1) } - fun nodes(nodes: List) { - foundNodes.clear() - nodes.forEach { addFoundNode(it) } + enum class NodeBuildStatus { + DISCOVERED, LOCALLY_BUILDING, LOCALLY_BUILT, REMOTE_PUSHING, REMOTE_PUSHED, INSTANTIATING, INSTANTIATED, } - fun notaries(notaries: List) { - foundNotaries.clear() - notaries.forEach { runLater { foundNotaries.add(it) } } + enum class NodeType { + NODE, NOTARY } - var baseDir = SimpleObjectProperty(null) + class State : Controller() { + val foundNodes = Collections.synchronizedList(ArrayList()).observable() + val foundNotaries = Collections.synchronizedList(ArrayList()).observable() + val networkContext = SimpleObjectProperty(null) + val unsortedNodes = Collections.synchronizedList(ArrayList()).observable() + val sortedNodes = SortedList(unsortedNodes, Comparator { 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) { + 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) { + foundNotaries.clear() + notaries.forEach { + runLater { + foundNotaries.add(it) + unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NOTARY)) + } + } + + } + + var baseDir = SimpleObjectProperty(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) { + 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) \ No newline at end of file +data class FoundNodeTableEntry(val id: String, @Volatile var count: Int = 1) \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt index cc71845369..841022f502 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt @@ -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) = Application.launch(Gui::class.java, *args) + override fun start(stage: Stage) { + super.start(stage) + stage.scene.stylesheets.add("/views/bootstrapper.css") } } diff --git a/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css b/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css new file mode 100644 index 0000000000..b02c3130b5 --- /dev/null +++ b/tools/network-bootstrapper/src/main/resources/views/bootstrapper.css @@ -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; +} \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/resources/views/cordalogo.png b/tools/network-bootstrapper/src/main/resources/views/cordalogo.png new file mode 100644 index 0000000000..5c97ca01e9 Binary files /dev/null and b/tools/network-bootstrapper/src/main/resources/views/cordalogo.png differ diff --git a/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml b/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml new file mode 100644 index 0000000000..c62993fd25 --- /dev/null +++ b/tools/network-bootstrapper/src/main/resources/views/mainPane.fxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +