From 38e57d63427eeb213ccfaeb2c619389a4e4ef1da Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 14 Feb 2017 17:14:54 +0000 Subject: [PATCH] CORPRIV-661: Allow profiles to be loaded into DemoBench. --- tools/demobench/build.gradle | 4 + .../net/corda/demobench/model/DBViewer.kt | 11 +- .../net/corda/demobench/model/Explorer.kt | 12 +- .../net/corda/demobench/model/NodeConfig.kt | 76 ++++++------ .../corda/demobench/model/NodeController.kt | 54 ++++++--- .../net/corda/demobench/model/WebServer.kt | 8 +- .../demobench/profile/ProfileController.kt | 110 ++++++++++++++++++ .../kotlin/net/corda/demobench/pty/R3Pty.kt | 4 +- .../kotlin/net/corda/demobench/rpc/NodeRPC.kt | 10 +- .../corda/demobench/views/DemoBenchView.kt | 54 +++++++-- .../net/corda/demobench/views/NodeTabView.kt | 34 ++++-- .../corda/demobench/views/DemoBenchView.fxml | 36 +++--- 12 files changed, 318 insertions(+), 95 deletions(-) create mode 100644 tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 682971f731..4336a3e1ed 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -6,6 +6,7 @@ buildscript { ext.guava_version = '14.0.1' ext.slf4j_version = '1.7.22' ext.logback_version = '1.1.10' + ext.controlsfx_version = '8.40.12' ext.java_home = System.properties.'java.home' ext.pkg_source = "$buildDir/packagesrc" @@ -52,6 +53,9 @@ dependencies { compile "no.tornado:tornadofx:$tornadofx_version" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + // Controls FX: more java FX components http://fxexperience.com/controlsfx/ + compile "org.controlsfx:controlsfx:$controlsfx_version" + // ONLY USING THE RPC CLIENT!? compile project(':node') diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt index b6ba3f81ae..6047cd5ec0 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt @@ -8,11 +8,12 @@ import java.util.concurrent.Executors import kotlin.reflect.jvm.jvmName class DBViewer : AutoCloseable { - private val log = loggerFor() + private companion object { + val log = loggerFor() + } private val webServer: Server private val pool = Executors.newCachedThreadPool() - private val t = Thread("DBViewer") init { val ws = LocalWebServer() @@ -23,15 +24,15 @@ class DBViewer : AutoCloseable { webServer.stop() } - t.run { + pool.submit { webServer.start() } } override fun close() { - webServer.shutdown() + log.info("Shutting down") pool.shutdown() - t.join() + webServer.shutdown() } fun openBrowser(h2Port: Int) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt index c70fe426ed..8ba46f4e06 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt @@ -4,7 +4,9 @@ import net.corda.demobench.loggerFor import java.util.concurrent.Executors class Explorer(val explorerController: ExplorerController) : AutoCloseable { - private val log = loggerFor() + private companion object { + val log = loggerFor() + } private val executor = Executors.newSingleThreadExecutor() private var process: Process? = null @@ -21,8 +23,8 @@ class Explorer(val explorerController: ExplorerController) : AutoCloseable { val p = explorerController.process( "--host=localhost", "--port=${config.artemisPort}", - "--username=${config.user["user"]}", - "--password=${config.user["password"]}", + "--username=${config.users[0]["user"]}", + "--password=${config.users[0]["password"]}", "--certificatesDir=${config.ssl.certificatesDirectory}", "--keyStorePassword=${config.ssl.keyStorePassword}", "--trustStorePassword=${config.ssl.trustStorePassword}") @@ -51,9 +53,9 @@ class Explorer(val explorerController: ExplorerController) : AutoCloseable { process?.destroy() } - private fun safeClose(c: AutoCloseable?) { + private fun safeClose(c: AutoCloseable) { try { - c?.close() + c.close() } catch (e: Exception) { log.error("Failed to close stream: '{}'", e.message) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 25d96f9400..7c6aac3464 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -1,12 +1,9 @@ package net.corda.demobench.model -import com.typesafe.config.Config -import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigValue -import com.typesafe.config.ConfigValueFactory -import net.corda.node.services.config.SSLConfiguration +import com.typesafe.config.* import java.lang.String.join import java.nio.file.Path +import net.corda.node.services.config.SSLConfiguration class NodeConfig( baseDir: Path, @@ -15,21 +12,26 @@ class NodeConfig( val nearestCity: String, val webPort: Int, val h2Port: Int, - val extraServices: List + val extraServices: List, + val users: List> = listOf(defaultUser) ) : NetworkMapConfig(legalName, artemisPort) { + companion object { + val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + + val defaultUser: Map = mapOf( + "user" to "guest", + "password" to "letmein", + "permissions" to listOf( + "StartFlow.net.corda.flows.CashFlow", + "StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester" + ) + ) + } + val nodeDir: Path = baseDir.resolve(key) val explorerDir: Path = baseDir.resolve("$key-explorer") - val user: Map = mapOf( - "user" to "guest", - "password" to "letmein", - "permissions" to listOf( - "StartFlow.net.corda.flows.CashFlow", - "StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester" - ) - ) - val ssl: SSLConfiguration = object : SSLConfiguration { override val certificatesDirectory: Path = nodeDir.resolve("certificates") override val trustStorePassword: String = "trustpass" @@ -40,29 +42,35 @@ class NodeConfig( var state: NodeState = NodeState.STARTING - /* - * The configuration object depends upon the networkMap, - * which is mutable. - */ - val toFileConfig: Config - get() = ConfigFactory.empty() - .withValue("myLegalName", valueFor(legalName)) - .withValue("artemisAddress", addressValueFor(artemisPort)) - .withValue("nearestCity", valueFor(nearestCity)) - .withValue("extraAdvertisedServiceIds", valueFor(join(",", extraServices))) - .withFallback(optional("networkMapService", networkMap, { - c, n -> c.withValue("address", addressValueFor(n.artemisPort)) - .withValue("legalName", valueFor(n.legalName)) - } )) - .withValue("webAddress", addressValueFor(webPort)) - .withValue("rpcUsers", valueFor(listOf(user))) - .withValue("h2port", valueFor(h2Port)) - .withValue("useTestClock", valueFor(true)) - val isCashIssuer: Boolean = extraServices.any { it.startsWith("corda.issuer.") } + fun isNetworkMap(): Boolean = networkMap == null + + /* + * The configuration object depends upon the networkMap, + * which is mutable. + */ + fun toFileConfig(): Config = ConfigFactory.empty() + .withValue("myLegalName", valueFor(legalName)) + .withValue("artemisAddress", addressValueFor(artemisPort)) + .withValue("nearestCity", valueFor(nearestCity)) + .withValue("extraAdvertisedServiceIds", valueFor(join(",", extraServices))) + .withFallback(optional("networkMapService", networkMap, { + c, n -> c.withValue("address", addressValueFor(n.artemisPort)) + .withValue("legalName", valueFor(n.legalName)) + } )) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcUsers", valueFor(users)) + .withValue("h2port", valueFor(h2Port)) + .withValue("useTestClock", valueFor(true)) + + fun toText() = toFileConfig().root().render(renderOptions) + + fun moveTo(baseDir: Path) = NodeConfig( + baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users + ) } private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 0d6cb12ca6..a946f795b6 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,18 +1,17 @@ package net.corda.demobench.model -import com.typesafe.config.ConfigRenderOptions +import java.io.IOException import java.lang.management.ManagementFactory +import java.net.ServerSocket import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import net.corda.demobench.pty.R3Pty import tornadofx.Controller -import java.io.IOException -import java.net.ServerSocket class NodeController : Controller() { - private companion object Data { + private companion object { const val FIRST_PORT = 10000 const val MIN_PORT = 1024 const val MAX_PORT = 65535 @@ -20,9 +19,7 @@ class NodeController : Controller() { private val jvm by inject() - private val localDir = SimpleDateFormat("yyyyMMddHHmmss") - .format(Date(ManagementFactory.getRuntimeMXBean().startTime)) - private val baseDir = jvm.userHome.resolve("demobench").resolve(localDir) + private var baseDir = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private val pluginDir = jvm.applicationDir.resolve("plugins") private val bankOfCorda = pluginDir.resolve("bank-of-corda.jar").toFile() @@ -30,13 +27,15 @@ class NodeController : Controller() { private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar") private val command = jvm.commandFor(cordaPath) - private val renderOptions = ConfigRenderOptions.defaults().setOriginComments(false) - private val nodes = ConcurrentHashMap() private val port = AtomicInteger(FIRST_PORT) private var networkMapConfig: NetworkMapConfig? = null + val activeNodes: List get() = nodes.values.filter { + it.state == NodeState.RUNNING + } + init { log.info("Base directory: $baseDir") log.info("Corda JAR: $cordaPath") @@ -75,7 +74,7 @@ class NodeController : Controller() { val nextPort: Int get() = port.andIncrement fun isPortAvailable(port: Int): Boolean { - if ((port >= MIN_PORT) && (port <= MAX_PORT)) { + if (isPortValid(port)) { try { ServerSocket(port).close() return true @@ -87,6 +86,8 @@ class NodeController : Controller() { } } + fun isPortValid(port: Int): Boolean = (port >= MIN_PORT) && (port <= MAX_PORT) + fun keyExists(key: String) = nodes.keys.contains(key) fun nameExists(name: String) = keyExists(toKey(name)) @@ -105,17 +106,16 @@ class NodeController : Controller() { fun runCorda(pty: R3Pty, config: NodeConfig): Boolean { val nodeDir = config.nodeDir.toFile() - if (nodeDir.mkdirs()) { + if (nodeDir.isDirectory || nodeDir.mkdirs()) { try { // Write this node's configuration file into its working directory. val confFile = nodeDir.resolve("node.conf") - val fileData = config.toFileConfig - confFile.writeText(fileData.root().render(renderOptions)) + confFile.writeText(config.toText()) // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. if (config.isCashIssuer && bankOfCorda.isFile) { log.info("Installing 'Bank of Corda' plugin") - bankOfCorda.copyTo(nodeDir.resolve("plugins").resolve(bankOfCorda.name)) + bankOfCorda.copyTo(nodeDir.resolve("plugins").resolve(bankOfCorda.name), overwrite=true) } // Execute the Corda node @@ -131,4 +131,30 @@ class NodeController : Controller() { } } + fun reset() { + baseDir = baseDirFor(System.currentTimeMillis()) + log.info("Changed base directory: $baseDir") + + // Wipe out any knowledge of previous nodes. + networkMapConfig = null + nodes.clear() + } + + fun register(config: NodeConfig): Boolean { + if (nodes.putIfAbsent(config.key, config) != null) { + return false + } + + if ((networkMapConfig == null) && config.isNetworkMap()) { + networkMapConfig = config + } + + return true + } + + fun relocate(config: NodeConfig) = config.moveTo(baseDir) + + private fun baseDirFor(time: Long) = jvm.userHome.resolve("demobench").resolve(localFor(time)) + private fun localFor(time: Long) = SimpleDateFormat("yyyyMMddHHmmss").format(Date(time)) + } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt index 1dda1045d1..2e38b9517e 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt @@ -4,7 +4,9 @@ import net.corda.demobench.loggerFor import java.util.concurrent.Executors class WebServer(val webServerController: WebServerController) : AutoCloseable { - private val log = loggerFor() + private companion object { + val log = loggerFor() + } private val executor = Executors.newSingleThreadExecutor() private var process: Process? = null @@ -44,9 +46,9 @@ class WebServer(val webServerController: WebServerController) : AutoCloseable { process?.destroy() } - private fun safeClose(c: AutoCloseable?) { + private fun safeClose(c: AutoCloseable) { try { - c?.close() + c.close() } catch (e: Exception) { log.error("Failed to close stream: '{}'", e.message) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt new file mode 100644 index 0000000000..2bd2024d96 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -0,0 +1,110 @@ +package net.corda.demobench.profile + +import com.google.common.net.HostAndPort +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import javafx.stage.FileChooser +import javafx.stage.FileChooser.ExtensionFilter +import net.corda.demobench.model.* +import tornadofx.Controller + +class ProfileController : Controller() { + + private val jvm by inject() + private val baseDir = jvm.userHome.resolve("demobench") + private val nodeController by inject() + private val serviceController by inject() + private val chooser = FileChooser() + + init { + chooser.initialDirectory = baseDir.toFile() + chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.zip)", "*.zip", "*.ZIP")) + } + + fun saveAs() { + log.info("Save as") + } + + fun save() { + log.info("Save") + } + + fun openProfile(): List? { + val chosen = chooser.showOpenDialog(null) ?: return null + log.info("Selected profile: ${chosen}") + + val configs = LinkedList() + + FileSystems.newFileSystem(chosen.toPath(), null).use { + fs -> fs.rootDirectories.forEach { + root -> Files.walk(root).forEach { + if ((it.nameCount == 2) && ("node.conf" == it.fileName.toString())) { + try { + configs.add(toNodeConfig(parse(it))) + } catch (e: Exception) { + log.severe("Failed to parse '$it': ${e.message}") + throw e + } + } + } + } + } + + return configs + } + + private fun toNodeConfig(config: Config): NodeConfig { + val artemisPort = config.parsePort("artemisAddress") + val webPort = config.parsePort("webAddress") + val h2Port = config.getInt("h2port") + val extraServices = config.parseExtraServices("extraAdvertisedServiceIds") + + val nodeConfig = NodeConfig( + baseDir, // temporary value + config.getString("myLegalName"), + artemisPort, + config.getString("nearestCity"), + webPort, + h2Port, + extraServices, + config.getObjectList("rpcUsers").map { it.unwrapped() }.toList() + ) + + if (config.hasPath("networkMapService")) { + val nmap = config.getConfig("networkMapService") + nodeConfig.networkMap = NetworkMapConfig(nmap.getString("legalName"), nmap.parsePort("address")) + } + + return nodeConfig + } + + private fun parse(path: Path): Config = Files.newBufferedReader(path).use { + return ConfigFactory.parseReader(it) + } + + private fun Config.parsePort(path: String): Int { + val address = this.getString(path) + val port = HostAndPort.fromString(address).port + if (!nodeController.isPortValid(port)) { + throw IllegalArgumentException("Invalid port $port from '$path'.") + } + return port + } + + private fun Config.parseExtraServices(path: String): List { + val services = serviceController.services.toSortedSet() + return this.getString(path).split(",").filter { + !it.isNullOrEmpty() + }.map { + if (!services.contains(it)) { + throw IllegalArgumentException("Unknown service '$it'.") + } else { + it + } + }.toList() + } +} \ No newline at end of file diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index ba33c6fb55..4cfb04f520 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -13,7 +13,9 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension, val onExit: () -> Unit) : AutoCloseable { - private val log = loggerFor() + private companion object { + val log = loggerFor() + } private val executor = Executors.newSingleThreadExecutor() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index b6f4065560..abfec68131 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -9,10 +9,10 @@ import net.corda.demobench.model.NodeConfig import net.corda.node.services.messaging.CordaRPCClient class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable { - private val log = loggerFor() - companion object Data { - private val ONE_SECOND = SECONDS.toMillis(1) + private companion object Data { + val log = loggerFor() + val ONE_SECOND = SECONDS.toMillis(1) } private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.artemisPort), config.ssl) @@ -22,8 +22,8 @@ class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Un val setupTask = object : TimerTask() { override fun run() { try { - rpcClient.start(config.user.getOrElse("user") { "none" } as String, - config.user.getOrElse("password") { "none" } as String) + rpcClient.start(config.users[0].getOrElse("user") { "none" } as String, + config.users[0].getOrElse("password") { "none" } as String) val ops = rpcClient.proxy() // Cancel the "setup" task now that we've created the RPC client. diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt index 81c5db5672..a2ea53aebf 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt @@ -4,17 +4,27 @@ import java.util.* import javafx.application.Platform import javafx.scene.Parent import javafx.scene.control.Button +import javafx.scene.control.MenuItem import javafx.scene.control.Tab import javafx.scene.control.TabPane +import net.corda.demobench.model.NodeConfig +import net.corda.demobench.model.NodeController +import net.corda.demobench.profile.ProfileController import net.corda.demobench.ui.CloseableTab +import org.controlsfx.dialog.ExceptionDialog import tornadofx.* class DemoBenchView : View("Corda Demo Bench") { override val root by fxml() + private val profileController by inject() + private val nodeController by inject() private val addNodeButton by fxid - - + + + + + + + + + + + + + + +