CORPRIV-661: Allow profiles to be loaded into DemoBench.

This commit is contained in:
Chris Rankin 2017-02-14 17:14:54 +00:00
parent 0ae5713a47
commit 38e57d6342
12 changed files with 318 additions and 95 deletions

View File

@ -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')

View File

@ -8,11 +8,12 @@ import java.util.concurrent.Executors
import kotlin.reflect.jvm.jvmName
class DBViewer : AutoCloseable {
private val log = loggerFor<DBViewer>()
private companion object {
val log = loggerFor<DBViewer>()
}
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) {

View File

@ -4,7 +4,9 @@ import net.corda.demobench.loggerFor
import java.util.concurrent.Executors
class Explorer(val explorerController: ExplorerController) : AutoCloseable {
private val log = loggerFor<Explorer>()
private companion object {
val log = loggerFor<Explorer>()
}
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)
}

View File

@ -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<String>
val extraServices: List<String>,
val users: List<Map<String, Any>> = listOf(defaultUser)
) : NetworkMapConfig(legalName, artemisPort) {
companion object {
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
val defaultUser: Map<String, Any> = 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<String, Any> = 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 <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)

View File

@ -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<JVMConfig>()
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<String, NodeConfig>()
private val port = AtomicInteger(FIRST_PORT)
private var networkMapConfig: NetworkMapConfig? = null
val activeNodes: List<NodeConfig> 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))
}

View File

@ -4,7 +4,9 @@ import net.corda.demobench.loggerFor
import java.util.concurrent.Executors
class WebServer(val webServerController: WebServerController) : AutoCloseable {
private val log = loggerFor<WebServer>()
private companion object {
val log = loggerFor<WebServer>()
}
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)
}

View File

@ -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<JVMConfig>()
private val baseDir = jvm.userHome.resolve("demobench")
private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>()
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<NodeConfig>? {
val chosen = chooser.showOpenDialog(null) ?: return null
log.info("Selected profile: ${chosen}")
val configs = LinkedList<NodeConfig>()
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<String> {
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()
}
}

View File

@ -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<R3Pty>()
private companion object {
val log = loggerFor<R3Pty>()
}
private val executor = Executors.newSingleThreadExecutor()

View File

@ -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<NodeRPC>()
companion object Data {
private val ONE_SECOND = SECONDS.toMillis(1)
private companion object Data {
val log = loggerFor<NodeRPC>()
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.

View File

@ -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<Parent>()
private val profileController by inject<ProfileController>()
private val nodeController by inject<NodeController>()
private val addNodeButton by fxid<Button>()
private val nodeTabPane by fxid<TabPane>()
private val menuOpen by fxid<MenuItem>()
private val menuSave by fxid<MenuItem>()
private val menuSaveAs by fxid<MenuItem>()
init {
importStylesheet("/net/corda/demobench/style.css")
@ -22,13 +32,33 @@ class DemoBenchView : View("Corda Demo Bench") {
primaryStage.setOnCloseRequest {
log.info("Exiting")
// Prevent any new NodeTabViews from being created.
addNodeButton.isDisable = true
closeAllTabs()
Platform.exit()
}
menuSaveAs.setOnAction {
profileController.saveAs()
}
menuSave.setOnAction {
profileController.save()
}
menuOpen.setOnAction {
try {
val profile = profileController.openProfile()
if (profile != null) {
loadProfile(profile)
}
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
}
}
addNodeButton.setOnAction {
val nodeTab = createNodeTab()
nodeTabPane.selectionModel.select(nodeTab)
val nodeTabView = createNodeTabView(true)
nodeTabPane.selectionModel.select(nodeTabView.nodeTab)
// Prevent us from creating new nodes until we have created the Network Map
addNodeButton.isDisable = true
@ -42,12 +72,22 @@ class DemoBenchView : View("Corda Demo Bench") {
}
}
fun createNodeTab(): CloseableTab {
val nodeTabView = find<NodeTabView>()
val nodeTab = nodeTabView.nodeTab
fun createNodeTabView(showConfig: Boolean): NodeTabView {
val nodeTabView = find<NodeTabView>(mapOf("showConfig" to showConfig))
nodeTabPane.tabs.add(nodeTabView.nodeTab)
return nodeTabView
}
nodeTabPane.tabs.add(nodeTab)
return nodeTab
fun loadProfile(nodes: List<NodeConfig>) {
closeAllTabs()
nodeController.reset()
nodes.forEach {
val nodeTabView = createNodeTabView(false)
nodeTabView.launch(nodeController.relocate(it))
}
enableAddNodes()
}
fun enableAddNodes() {

View File

@ -15,8 +15,9 @@ class NodeTabView : Fragment() {
override val root = stackpane {}
private val main by inject<DemoBenchView>()
private val showConfig by param<Boolean>()
private companion object Data {
private companion object {
val INTEGER_FORMAT = DecimalFormat()
val NOT_NUMBER = "[^\\d]".toRegex()
}
@ -27,6 +28,8 @@ class NodeTabView : Fragment() {
private val nodeTerminalView = find<NodeTerminalView>()
private val nodeConfigView = stackpane {
isVisible = showConfig
form {
fieldset("Configuration") {
field("Node Name") {
@ -179,19 +182,34 @@ class NodeTabView : Fragment() {
model.h2Port.value = nodeController.nextPort
}
/**
* Launches a Corda node that was configured via the form.
*/
fun launch() {
model.commit()
val config = nodeController.validate(model.item)
if (config != null) {
nodeConfigView.isVisible = false
nodeTab.text = config.legalName
nodeTerminalView.open(config, onExit = { onTerminalExit(config) })
launchNode(config)
}
}
nodeTab.setOnSelectionChanged {
if (nodeTab.isSelected) {
// Doesn't work yet
nodeTerminalView.refreshTerminal()
}
/**
* Launches a preconfigured Corda node, e.g. from a saved profile.
*/
fun launch(config: NodeConfig) {
nodeController.register(config)
launchNode(config)
}
private fun launchNode(config: NodeConfig) {
nodeTab.text = config.legalName
nodeTerminalView.open(config, onExit = { onTerminalExit(config) })
nodeTab.setOnSelectionChanged {
if (nodeTab.isSelected) {
// Doesn't work yet
nodeTerminalView.refreshTerminal()
}
}
}

View File

@ -2,19 +2,29 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<StackPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
<children>
<TabPane fx:id="nodeTabPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0">
<tabs>
</tabs>
</TabPane>
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
<StackPane.margin>
<Insets right="5.0" top="5.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
<VBox xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<MenuBar>
<Menu text="File">
<MenuItem fx:id="menuOpen" text="Open"/>
<MenuItem fx:id="menuSave" text="Save"/>
<MenuItem fx:id="menuSaveAs" text="Save As"/>
</Menu>
</MenuBar>
<StackPane>
<children>
<TabPane fx:id="nodeTabPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0"/>
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
<StackPane.margin>
<Insets right="5.0" top="5.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
</VBox>