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.guava_version = '14.0.1'
ext.slf4j_version = '1.7.22' ext.slf4j_version = '1.7.22'
ext.logback_version = '1.1.10' ext.logback_version = '1.1.10'
ext.controlsfx_version = '8.40.12'
ext.java_home = System.properties.'java.home' ext.java_home = System.properties.'java.home'
ext.pkg_source = "$buildDir/packagesrc" ext.pkg_source = "$buildDir/packagesrc"
@ -52,6 +53,9 @@ dependencies {
compile "no.tornado:tornadofx:$tornadofx_version" compile "no.tornado:tornadofx:$tornadofx_version"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_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!? // ONLY USING THE RPC CLIENT!?
compile project(':node') compile project(':node')

View File

@ -8,11 +8,12 @@ import java.util.concurrent.Executors
import kotlin.reflect.jvm.jvmName import kotlin.reflect.jvm.jvmName
class DBViewer : AutoCloseable { class DBViewer : AutoCloseable {
private val log = loggerFor<DBViewer>() private companion object {
val log = loggerFor<DBViewer>()
}
private val webServer: Server private val webServer: Server
private val pool = Executors.newCachedThreadPool() private val pool = Executors.newCachedThreadPool()
private val t = Thread("DBViewer")
init { init {
val ws = LocalWebServer() val ws = LocalWebServer()
@ -23,15 +24,15 @@ class DBViewer : AutoCloseable {
webServer.stop() webServer.stop()
} }
t.run { pool.submit {
webServer.start() webServer.start()
} }
} }
override fun close() { override fun close() {
webServer.shutdown() log.info("Shutting down")
pool.shutdown() pool.shutdown()
t.join() webServer.shutdown()
} }
fun openBrowser(h2Port: Int) { fun openBrowser(h2Port: Int) {

View File

@ -4,7 +4,9 @@ import net.corda.demobench.loggerFor
import java.util.concurrent.Executors import java.util.concurrent.Executors
class Explorer(val explorerController: ExplorerController) : AutoCloseable { class Explorer(val explorerController: ExplorerController) : AutoCloseable {
private val log = loggerFor<Explorer>() private companion object {
val log = loggerFor<Explorer>()
}
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null private var process: Process? = null
@ -21,8 +23,8 @@ class Explorer(val explorerController: ExplorerController) : AutoCloseable {
val p = explorerController.process( val p = explorerController.process(
"--host=localhost", "--host=localhost",
"--port=${config.artemisPort}", "--port=${config.artemisPort}",
"--username=${config.user["user"]}", "--username=${config.users[0]["user"]}",
"--password=${config.user["password"]}", "--password=${config.users[0]["password"]}",
"--certificatesDir=${config.ssl.certificatesDirectory}", "--certificatesDir=${config.ssl.certificatesDirectory}",
"--keyStorePassword=${config.ssl.keyStorePassword}", "--keyStorePassword=${config.ssl.keyStorePassword}",
"--trustStorePassword=${config.ssl.trustStorePassword}") "--trustStorePassword=${config.ssl.trustStorePassword}")
@ -51,9 +53,9 @@ class Explorer(val explorerController: ExplorerController) : AutoCloseable {
process?.destroy() process?.destroy()
} }
private fun safeClose(c: AutoCloseable?) { private fun safeClose(c: AutoCloseable) {
try { try {
c?.close() c.close()
} catch (e: Exception) { } catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message) log.error("Failed to close stream: '{}'", e.message)
} }

View File

@ -1,12 +1,9 @@
package net.corda.demobench.model package net.corda.demobench.model
import com.typesafe.config.Config import com.typesafe.config.*
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory
import net.corda.node.services.config.SSLConfiguration
import java.lang.String.join import java.lang.String.join
import java.nio.file.Path import java.nio.file.Path
import net.corda.node.services.config.SSLConfiguration
class NodeConfig( class NodeConfig(
baseDir: Path, baseDir: Path,
@ -15,21 +12,26 @@ class NodeConfig(
val nearestCity: String, val nearestCity: String,
val webPort: Int, val webPort: Int,
val h2Port: Int, val h2Port: Int,
val extraServices: List<String> val extraServices: List<String>,
val users: List<Map<String, Any>> = listOf(defaultUser)
) : NetworkMapConfig(legalName, artemisPort) { ) : 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 nodeDir: Path = baseDir.resolve(key)
val explorerDir: Path = baseDir.resolve("$key-explorer") 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 { val ssl: SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory: Path = nodeDir.resolve("certificates") override val certificatesDirectory: Path = nodeDir.resolve("certificates")
override val trustStorePassword: String = "trustpass" override val trustStorePassword: String = "trustpass"
@ -40,29 +42,35 @@ class NodeConfig(
var state: NodeState = NodeState.STARTING 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 { val isCashIssuer: Boolean = extraServices.any {
it.startsWith("corda.issuer.") 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) private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)

View File

@ -1,18 +1,17 @@
package net.corda.demobench.model package net.corda.demobench.model
import com.typesafe.config.ConfigRenderOptions import java.io.IOException
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.ServerSocket
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import net.corda.demobench.pty.R3Pty import net.corda.demobench.pty.R3Pty
import tornadofx.Controller import tornadofx.Controller
import java.io.IOException
import java.net.ServerSocket
class NodeController : Controller() { class NodeController : Controller() {
private companion object Data { private companion object {
const val FIRST_PORT = 10000 const val FIRST_PORT = 10000
const val MIN_PORT = 1024 const val MIN_PORT = 1024
const val MAX_PORT = 65535 const val MAX_PORT = 65535
@ -20,9 +19,7 @@ class NodeController : Controller() {
private val jvm by inject<JVMConfig>() private val jvm by inject<JVMConfig>()
private val localDir = SimpleDateFormat("yyyyMMddHHmmss") private var baseDir = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
.format(Date(ManagementFactory.getRuntimeMXBean().startTime))
private val baseDir = jvm.userHome.resolve("demobench").resolve(localDir)
private val pluginDir = jvm.applicationDir.resolve("plugins") private val pluginDir = jvm.applicationDir.resolve("plugins")
private val bankOfCorda = pluginDir.resolve("bank-of-corda.jar").toFile() 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 cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
private val command = jvm.commandFor(cordaPath) private val command = jvm.commandFor(cordaPath)
private val renderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
private val nodes = ConcurrentHashMap<String, NodeConfig>() private val nodes = ConcurrentHashMap<String, NodeConfig>()
private val port = AtomicInteger(FIRST_PORT) private val port = AtomicInteger(FIRST_PORT)
private var networkMapConfig: NetworkMapConfig? = null private var networkMapConfig: NetworkMapConfig? = null
val activeNodes: List<NodeConfig> get() = nodes.values.filter {
it.state == NodeState.RUNNING
}
init { init {
log.info("Base directory: $baseDir") log.info("Base directory: $baseDir")
log.info("Corda JAR: $cordaPath") log.info("Corda JAR: $cordaPath")
@ -75,7 +74,7 @@ class NodeController : Controller() {
val nextPort: Int get() = port.andIncrement val nextPort: Int get() = port.andIncrement
fun isPortAvailable(port: Int): Boolean { fun isPortAvailable(port: Int): Boolean {
if ((port >= MIN_PORT) && (port <= MAX_PORT)) { if (isPortValid(port)) {
try { try {
ServerSocket(port).close() ServerSocket(port).close()
return true 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 keyExists(key: String) = nodes.keys.contains(key)
fun nameExists(name: String) = keyExists(toKey(name)) fun nameExists(name: String) = keyExists(toKey(name))
@ -105,17 +106,16 @@ class NodeController : Controller() {
fun runCorda(pty: R3Pty, config: NodeConfig): Boolean { fun runCorda(pty: R3Pty, config: NodeConfig): Boolean {
val nodeDir = config.nodeDir.toFile() val nodeDir = config.nodeDir.toFile()
if (nodeDir.mkdirs()) { if (nodeDir.isDirectory || nodeDir.mkdirs()) {
try { try {
// Write this node's configuration file into its working directory. // Write this node's configuration file into its working directory.
val confFile = nodeDir.resolve("node.conf") val confFile = nodeDir.resolve("node.conf")
val fileData = config.toFileConfig confFile.writeText(config.toText())
confFile.writeText(fileData.root().render(renderOptions))
// Nodes cannot issue cash unless they contain the "Bank of Corda" plugin. // Nodes cannot issue cash unless they contain the "Bank of Corda" plugin.
if (config.isCashIssuer && bankOfCorda.isFile) { if (config.isCashIssuer && bankOfCorda.isFile) {
log.info("Installing 'Bank of Corda' plugin") 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 // 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 import java.util.concurrent.Executors
class WebServer(val webServerController: WebServerController) : AutoCloseable { class WebServer(val webServerController: WebServerController) : AutoCloseable {
private val log = loggerFor<WebServer>() private companion object {
val log = loggerFor<WebServer>()
}
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null private var process: Process? = null
@ -44,9 +46,9 @@ class WebServer(val webServerController: WebServerController) : AutoCloseable {
process?.destroy() process?.destroy()
} }
private fun safeClose(c: AutoCloseable?) { private fun safeClose(c: AutoCloseable) {
try { try {
c?.close() c.close()
} catch (e: Exception) { } catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message) 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 import java.util.concurrent.TimeUnit
class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension, val onExit: () -> Unit) : AutoCloseable { 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() private val executor = Executors.newSingleThreadExecutor()

View File

@ -9,10 +9,10 @@ import net.corda.demobench.model.NodeConfig
import net.corda.node.services.messaging.CordaRPCClient import net.corda.node.services.messaging.CordaRPCClient
class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable { class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable {
private val log = loggerFor<NodeRPC>()
companion object Data { private companion object Data {
private val ONE_SECOND = SECONDS.toMillis(1) val log = loggerFor<NodeRPC>()
val ONE_SECOND = SECONDS.toMillis(1)
} }
private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.artemisPort), config.ssl) 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() { val setupTask = object : TimerTask() {
override fun run() { override fun run() {
try { try {
rpcClient.start(config.user.getOrElse("user") { "none" } as String, rpcClient.start(config.users[0].getOrElse("user") { "none" } as String,
config.user.getOrElse("password") { "none" } as String) config.users[0].getOrElse("password") { "none" } as String)
val ops = rpcClient.proxy() val ops = rpcClient.proxy()
// Cancel the "setup" task now that we've created the RPC client. // 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.application.Platform
import javafx.scene.Parent import javafx.scene.Parent
import javafx.scene.control.Button import javafx.scene.control.Button
import javafx.scene.control.MenuItem
import javafx.scene.control.Tab import javafx.scene.control.Tab
import javafx.scene.control.TabPane 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 net.corda.demobench.ui.CloseableTab
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.* import tornadofx.*
class DemoBenchView : View("Corda Demo Bench") { class DemoBenchView : View("Corda Demo Bench") {
override val root by fxml<Parent>() 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 addNodeButton by fxid<Button>()
private val nodeTabPane by fxid<TabPane>() 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 { init {
importStylesheet("/net/corda/demobench/style.css") importStylesheet("/net/corda/demobench/style.css")
@ -22,13 +32,33 @@ class DemoBenchView : View("Corda Demo Bench") {
primaryStage.setOnCloseRequest { primaryStage.setOnCloseRequest {
log.info("Exiting") log.info("Exiting")
// Prevent any new NodeTabViews from being created.
addNodeButton.isDisable = true
closeAllTabs() closeAllTabs()
Platform.exit() 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 { addNodeButton.setOnAction {
val nodeTab = createNodeTab() val nodeTabView = createNodeTabView(true)
nodeTabPane.selectionModel.select(nodeTab) nodeTabPane.selectionModel.select(nodeTabView.nodeTab)
// Prevent us from creating new nodes until we have created the Network Map // Prevent us from creating new nodes until we have created the Network Map
addNodeButton.isDisable = true addNodeButton.isDisable = true
@ -42,12 +72,22 @@ class DemoBenchView : View("Corda Demo Bench") {
} }
} }
fun createNodeTab(): CloseableTab { fun createNodeTabView(showConfig: Boolean): NodeTabView {
val nodeTabView = find<NodeTabView>() val nodeTabView = find<NodeTabView>(mapOf("showConfig" to showConfig))
val nodeTab = nodeTabView.nodeTab nodeTabPane.tabs.add(nodeTabView.nodeTab)
return nodeTabView
}
nodeTabPane.tabs.add(nodeTab) fun loadProfile(nodes: List<NodeConfig>) {
return nodeTab closeAllTabs()
nodeController.reset()
nodes.forEach {
val nodeTabView = createNodeTabView(false)
nodeTabView.launch(nodeController.relocate(it))
}
enableAddNodes()
} }
fun enableAddNodes() { fun enableAddNodes() {

View File

@ -15,8 +15,9 @@ class NodeTabView : Fragment() {
override val root = stackpane {} override val root = stackpane {}
private val main by inject<DemoBenchView>() private val main by inject<DemoBenchView>()
private val showConfig by param<Boolean>()
private companion object Data { private companion object {
val INTEGER_FORMAT = DecimalFormat() val INTEGER_FORMAT = DecimalFormat()
val NOT_NUMBER = "[^\\d]".toRegex() val NOT_NUMBER = "[^\\d]".toRegex()
} }
@ -27,6 +28,8 @@ class NodeTabView : Fragment() {
private val nodeTerminalView = find<NodeTerminalView>() private val nodeTerminalView = find<NodeTerminalView>()
private val nodeConfigView = stackpane { private val nodeConfigView = stackpane {
isVisible = showConfig
form { form {
fieldset("Configuration") { fieldset("Configuration") {
field("Node Name") { field("Node Name") {
@ -179,19 +182,34 @@ class NodeTabView : Fragment() {
model.h2Port.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort
} }
/**
* Launches a Corda node that was configured via the form.
*/
fun launch() { fun launch() {
model.commit() model.commit()
val config = nodeController.validate(model.item) val config = nodeController.validate(model.item)
if (config != null) { if (config != null) {
nodeConfigView.isVisible = false nodeConfigView.isVisible = false
nodeTab.text = config.legalName launchNode(config)
nodeTerminalView.open(config, onExit = { onTerminalExit(config) }) }
}
nodeTab.setOnSelectionChanged { /**
if (nodeTab.isSelected) { * Launches a preconfigured Corda node, e.g. from a saved profile.
// Doesn't work yet */
nodeTerminalView.refreshTerminal() 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.geometry.Insets?>
<?import javafx.scene.control.Button?> <?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.control.TabPane?>
<?import javafx.scene.layout.StackPane?> <?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"> <VBox xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<children> <MenuBar>
<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"> <Menu text="File">
<tabs> <MenuItem fx:id="menuOpen" text="Open"/>
</tabs> <MenuItem fx:id="menuSave" text="Save"/>
</TabPane> <MenuItem fx:id="menuSaveAs" text="Save As"/>
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT"> </Menu>
<StackPane.margin> </MenuBar>
<Insets right="5.0" top="5.0" /> <StackPane>
</StackPane.margin> <children>
</Button> <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"/>
</children> <Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
</StackPane> <StackPane.margin>
<Insets right="5.0" top="5.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
</VBox>