Merged in demobench (pull request #23)

Saving & loading of Cordapps with DemoBench profiles.

* CORPRIV-664: Implement saving/loading of Cordapps with profiles.

* CORPRIV-664: Refactor saving/loading plugins.

* CORPRIV-664: Add initial unit tests for model.

* CORPRIV-664: Add simple unit tests for NodeController.

* CORPRIV-664: Unit test enhancements, e.g. configure JUL properly.

* CORPRIV-664: Use Suite instead of abstract test class.

* CORPRIV-664: Allow Cordapps to be loaded when each Node is configured.

* CORPRIV-664: Document which checked Java exceptions are thrown.

* Write JavaPackager output into build/javapackage directory.

* CORPRIV-664: Document more checked Java exceptions.

* Refactor Web and Explorer classes into their own packages.

* Declare WebServer and Explorer constructors as "internal".

* Update packaging scripts: tell user where the installer is!

* CORPRIV-659: Set "system menu bar" property for MacOSX.

* CORPRIV-661: Use "*.profile" for profile files.

* Remove unnecessary <children/> elements, as they are defaults.

* Fix build breakage when on Windows.

* Tweaks for EXE packaging script.

* Change function to extension function.

* Code review fixes.

Approved-by: Clinton Alexander
This commit is contained in:
Chris Rankin 2017-03-13 13:31:38 +00:00
parent d52accb52c
commit c64ab4b7a5
41 changed files with 1236 additions and 320 deletions

View File

@ -10,6 +10,7 @@ buildscript {
ext.java_home = System.properties.'java.home' ext.java_home = System.properties.'java.home'
ext.pkg_source = "$buildDir/packagesrc" ext.pkg_source = "$buildDir/packagesrc"
ext.pkg_outDir = "$buildDir/javapackage"
ext.dist_source = "$pkg_source/demobench-$version" ext.dist_source = "$pkg_source/demobench-$version"
ext.pkg_version = "$version".indexOf('-') >= 0 ? "$version".substring(0, "$version".indexOf('-')) : version ext.pkg_version = "$version".indexOf('-') >= 0 ? "$version".substring(0, "$version".indexOf('-')) : version
@ -99,6 +100,10 @@ jar {
} }
} }
test {
systemProperty 'java.util.logging.config.class', 'net.corda.demobench.config.LoggingConfig'
}
distributions { distributions {
main() { main() {
contents { contents {
@ -134,7 +139,7 @@ distributions {
task javapackage(dependsOn: 'distZip') { task javapackage(dependsOn: 'distZip') {
doLast { doLast {
delete([pkg_source, "$buildDir/exedir"]) delete([pkg_source, pkg_outDir])
copy { copy {
from(zipTree(distZip.outputs.files.singleFile)) from(zipTree(distZip.outputs.files.singleFile))
@ -175,7 +180,7 @@ task javapackage(dependsOn: 'distZip') {
classpath: "$pkg_source:$java_home/../lib/ant-javafx.jar" classpath: "$pkg_source:$java_home/../lib/ant-javafx.jar"
) )
ant.deploy(nativeBundles: packageType, outdir: "$buildDir/exedir", outfile: 'DemoBench', verbose: 'true') { ant.deploy(nativeBundles: packageType, outdir: pkg_outDir, outfile: 'DemoBench', verbose: 'true') {
application(name: 'DemoBench', version: pkg_version, mainClass: mainClassName) application(name: 'DemoBench', version: pkg_version, mainClass: mainClassName)
info(title: 'DemoBench', vendor: 'R3', description: 'A sales and educational tool for demonstrating Corda.') info(title: 'DemoBench', vendor: 'R3', description: 'A sales and educational tool for demonstrating Corda.')
resources { resources {

View File

@ -7,4 +7,7 @@ if [ -z "$JAVA_HOME" -o ! -x $JAVA_HOME/bin/java ]; then
exit 1 exit 1
fi fi
exec $DIRNAME/gradlew -PpackageType=dmg javapackage $* $DIRNAME/../../gradlew -PpackageType=dmg javapackage $*
echo
echo "Wrote installer to '$(find build/javapackage/bundles -type f)'"
echo

View File

@ -8,10 +8,13 @@ if not defined JAVA_HOME goto NoJavaHome
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
call %DIRNAME%\gradlew -PpackageType=exe javapackage call %DIRNAME%\..\..\gradlew -PpackageType=exe javapackage
@echo
@echo "Wrote installer to %DIRNAME%\build\javapackage\bundles\"
@echo
goto end goto end
:NoJavaHome :NoJavaHome
echo "Please set JAVA_HOME correctly" @echo "Please set JAVA_HOME correctly"
:end :end

View File

@ -0,0 +1,13 @@
#!/bin/sh
DIRNAME=$(dirname $0)
if [ -z "$JAVA_HOME" -o ! -x $JAVA_HOME/bin/java ]; then
echo "Please set JAVA_HOME correctly"
exit 1
fi
$DIRNAME/../../gradlew -PpackageType=rpm javapackage $*
echo
echo "Wrote installer to '$(find $DIRNAME/build/javapackage/bundles -type f)'"
echo

View File

@ -0,0 +1,74 @@
package net.corda.demobench.explorer
import java.io.IOException
import java.util.concurrent.Executors
import net.corda.demobench.loggerFor
import net.corda.demobench.model.NodeConfig
import net.corda.demobench.model.forceDirectory
class Explorer internal constructor(private val explorerController: ExplorerController) : AutoCloseable {
private companion object {
val log = loggerFor<Explorer>()
}
private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null
@Throws(IOException::class)
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
val explorerDir = config.explorerDir.toFile()
if (!explorerDir.forceDirectory()) {
log.warn("Failed to create working directory '{}'", explorerDir.absolutePath)
onExit(config)
return
}
try {
val p = explorerController.process(
"--host=localhost",
"--port=${config.artemisPort}",
"--username=${config.users[0].user}",
"--password=${config.users[0].password}",
"--certificatesDir=${config.ssl.certificatesDirectory}",
"--keyStorePassword=${config.ssl.keyStorePassword}",
"--trustStorePassword=${config.ssl.trustStorePassword}")
.directory(explorerDir)
.start()
process = p
log.info("Launched Node Explorer for '{}'", config.legalName)
// Close these streams because no-one is using them.
safeClose(p.outputStream)
safeClose(p.inputStream)
safeClose(p.errorStream)
executor.submit {
val exitValue = p.waitFor()
process = null
log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue)
onExit(config)
}
} catch (e: IOException) {
log.error("Failed to launch Node Explorer for '{}': {}", config.legalName, e.message)
onExit(config)
throw e
}
}
override fun close() {
executor.shutdown()
process?.destroy()
}
private fun safeClose(c: AutoCloseable) {
try {
c.close()
} catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message)
}
}
}

View File

@ -1,5 +1,6 @@
package net.corda.demobench.model package net.corda.demobench.explorer
import net.corda.demobench.model.JVMConfig
import tornadofx.Controller import tornadofx.Controller
class ExplorerController : Controller() { class ExplorerController : Controller() {

View File

@ -1,64 +0,0 @@
package net.corda.demobench.model
import net.corda.demobench.loggerFor
import java.util.concurrent.Executors
class Explorer(val explorerController: ExplorerController) : AutoCloseable {
private companion object {
val log = loggerFor<Explorer>()
}
private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
val explorerDir = config.explorerDir.toFile()
if (!explorerDir.isDirectory && !explorerDir.mkdirs()) {
log.warn("Failed to create working directory '{}'", explorerDir.absolutePath)
onExit(config)
return
}
val p = explorerController.process(
"--host=localhost",
"--port=${config.artemisPort}",
"--username=${config.users[0]["user"]}",
"--password=${config.users[0]["password"]}",
"--certificatesDir=${config.ssl.certificatesDirectory}",
"--keyStorePassword=${config.ssl.keyStorePassword}",
"--trustStorePassword=${config.ssl.trustStorePassword}")
.directory(explorerDir)
.start()
process = p
log.info("Launched Node Explorer for '{}'", config.legalName)
// Close these streams because no-one is using them.
safeClose(p.outputStream)
safeClose(p.inputStream)
safeClose(p.errorStream)
executor.submit {
val exitValue = p.waitFor()
process = null
log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue)
onExit(config)
}
}
override fun close() {
executor.shutdown()
process?.destroy()
}
private fun safeClose(c: AutoCloseable) {
try {
c.close()
} catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message)
}
}
}

View File

@ -0,0 +1,7 @@
package net.corda.demobench.model
import java.nio.file.Path
interface HasPlugins {
val pluginDir: Path
}

View File

@ -0,0 +1,72 @@
package net.corda.demobench.model
import com.google.common.net.HostAndPort
import com.typesafe.config.Config
import java.io.IOException
import java.nio.file.*
import tornadofx.Controller
class InstallFactory : Controller() {
private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>()
@Throws(IOException::class)
fun toInstallConfig(config: Config, baseDir: Path): InstallConfig {
val artemisPort = config.parsePort("artemisAddress")
val webPort = config.parsePort("webAddress")
val h2Port = config.getInt("h2port")
val extraServices = config.parseExtraServices("extraAdvertisedServiceIds")
val tempDir = Files.createTempDirectory(baseDir, ".node")
val nodeConfig = NodeConfig(
tempDir,
config.getString("myLegalName"),
artemisPort,
config.getString("nearestCity"),
webPort,
h2Port,
extraServices,
config.getObjectList("rpcUsers").map { toUser(it.unwrapped()) }.toList()
)
if (config.hasPath("networkMapService")) {
val nmap = config.getConfig("networkMapService")
nodeConfig.networkMap = NetworkMapConfig(nmap.getString("legalName"), nmap.parsePort("address"))
} else {
log.info("Node '${nodeConfig.legalName}' is the network map")
}
return InstallConfig(tempDir, nodeConfig)
}
private fun Config.parsePort(path: String): Int {
val address = this.getString(path)
val port = HostAndPort.fromString(address).port
require(nodeController.isPortValid(port), { "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 { svc ->
require(svc in services, { "Unknown service '$svc'." } )
svc
}.toList()
}
}
/**
* Wraps the configuration information for a Node
* which isn't ready to be instantiated yet.
*/
class InstallConfig internal constructor(val baseDir: Path, private val config: NodeConfig) : HasPlugins {
val key = config.key
override val pluginDir: Path = baseDir.resolve("plugins")
fun deleteBaseDir(): Boolean = baseDir.toFile().deleteRecursively()
fun installTo(installDir: Path) = config.moveTo(installDir)
}

View File

@ -7,6 +7,7 @@ import tornadofx.Controller
class JVMConfig : Controller() { class JVMConfig : Controller() {
val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath() val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath()
val dataHome: Path = userHome.resolve("demobench")
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath() val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath()
@ -14,13 +15,12 @@ class JVMConfig : Controller() {
log.info("Java executable: $javaPath") log.info("Java executable: $javaPath")
} }
fun commandFor(jarPath: Path, vararg args: String): Array<String> { fun commandFor(jarPath: Path, vararg args: String): List<String> {
return arrayOf(javaPath.toString(), "-jar", jarPath.toString(), *args) return listOf(javaPath.toString(), "-jar", jarPath.toString(), *args)
} }
fun processFor(jarPath: Path, vararg args: String): ProcessBuilder { fun processFor(jarPath: Path, vararg args: String): ProcessBuilder {
return ProcessBuilder(commandFor(jarPath, *args).toList()) return ProcessBuilder(commandFor(jarPath, *args))
} }
} }

View File

@ -2,11 +2,11 @@ package net.corda.demobench.model
open class NetworkMapConfig(val legalName: String, val artemisPort: Int) { open class NetworkMapConfig(val legalName: String, val artemisPort: Int) {
private var keyValue = toKey(legalName) val key: String = legalName.toKey()
val key: String get() = keyValue
} }
private val WHITESPACE = "\\s++".toRegex() private val WHITESPACE = "\\s++".toRegex()
fun toKey(value: String) = value.replace(WHITESPACE, "").toLowerCase() fun String.stripWhitespace() = this.replace(WHITESPACE, "")
fun String.toKey() = this.stripWhitespace().toLowerCase()

View File

@ -2,7 +2,10 @@ package net.corda.demobench.model
import com.typesafe.config.* import com.typesafe.config.*
import java.lang.String.join import java.lang.String.join
import java.io.File
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import net.corda.node.services.config.SSLConfiguration import net.corda.node.services.config.SSLConfiguration
class NodeConfig( class NodeConfig(
@ -13,24 +16,16 @@ class NodeConfig(
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), val users: List<User> = listOf(user("guest")),
var networkMap: NetworkMapConfig? = null var networkMap: NetworkMapConfig? = null
) : NetworkMapConfig(legalName, artemisPort) { ) : NetworkMapConfig(legalName, artemisPort), HasPlugins {
companion object { companion object {
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) 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)
override val pluginDir: Path = nodeDir.resolve("plugins")
val explorerDir: Path = baseDir.resolve("$key-explorer") val explorerDir: Path = baseDir.resolve("$key-explorer")
val ssl: SSLConfiguration = object : SSLConfiguration { val ssl: SSLConfiguration = object : SSLConfiguration {
@ -61,7 +56,7 @@ class NodeConfig(
.withValue("legalName", valueFor(n.legalName)) .withValue("legalName", valueFor(n.legalName))
} )) } ))
.withValue("webAddress", addressValueFor(webPort)) .withValue("webAddress", addressValueFor(webPort))
.withValue("rpcUsers", valueFor(users)) .withValue("rpcUsers", valueFor(users.map(User::toMap).toList()))
.withValue("h2port", valueFor(h2Port)) .withValue("h2port", valueFor(h2Port))
.withValue("useTestClock", valueFor(true)) .withValue("useTestClock", valueFor(true))
@ -70,13 +65,25 @@ class NodeConfig(
fun moveTo(baseDir: Path) = NodeConfig( fun moveTo(baseDir: Path) = NodeConfig(
baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users, networkMap baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users, networkMap
) )
fun install(plugins: Collection<Path>) {
if (plugins.isNotEmpty() && pluginDir.toFile().forceDirectory()) {
plugins.forEach {
Files.copy(it, pluginDir.resolve(it.fileName.toString()), StandardCopyOption.REPLACE_EXISTING)
}
}
}
fun extendUserPermissions(permissions: Collection<String>) = users.forEach { it.extendPermissions(permissions) }
} }
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
private fun addressValueFor(port: Int) = valueFor("localhost:$port") private fun addressValueFor(port: Int) = valueFor("localhost:$port")
private fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config { private inline fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config {
val config = ConfigFactory.empty() val config = ConfigFactory.empty()
return if (obj == null) config else body(config, obj).atPath(path) return if (obj == null) config else body(config, obj).atPath(path)
} }
fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs()

View File

@ -3,28 +3,29 @@ package net.corda.demobench.model
import java.io.IOException import java.io.IOException
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.ServerSocket import java.net.ServerSocket
import java.nio.file.Files
import java.nio.file.Path
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import net.corda.demobench.plugin.PluginController
import net.corda.demobench.pty.R3Pty import net.corda.demobench.pty.R3Pty
import tornadofx.Controller import tornadofx.Controller
class NodeController : Controller() { class NodeController : Controller() {
private companion object { companion object {
const val firstPort = 10000 const val firstPort = 10000
const val minPort = 1024 const val minPort = 1024
const val maxPort = 65535 const val maxPort = 65535
} }
private val jvm by inject<JVMConfig>() private val jvm by inject<JVMConfig>()
private val pluginController by inject<PluginController>()
private var baseDir = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime) private var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
private val pluginDir = jvm.applicationDir.resolve("plugins") private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar")
private val command = jvm.commandFor(cordaPath).toTypedArray()
private val bankOfCorda = pluginDir.resolve("bank-of-corda.jar").toFile()
private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
private val command = jvm.commandFor(cordaPath)
private val nodes = LinkedHashMap<String, NodeConfig>() private val nodes = LinkedHashMap<String, NodeConfig>()
private val port = AtomicInteger(firstPort) private val port = AtomicInteger(firstPort)
@ -40,6 +41,9 @@ class NodeController : Controller() {
log.info("Corda JAR: $cordaPath") log.info("Corda JAR: $cordaPath")
} }
/**
* Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView].
*/
fun validate(nodeData: NodeData): NodeConfig? { fun validate(nodeData: NodeData): NodeConfig? {
val config = NodeConfig( val config = NodeConfig(
baseDir, baseDir,
@ -85,15 +89,15 @@ class NodeController : Controller() {
} }
} }
fun isPortValid(port: Int): Boolean = (port >= minPort) && (port <= maxPort) fun isPortValid(port: Int) = (port >= minPort) && (port <= maxPort)
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(name.toKey())
fun hasNetworkMap(): Boolean = networkMapConfig != null fun hasNetworkMap(): Boolean = networkMapConfig != null
fun chooseNetworkMap(config: NodeConfig) { private fun chooseNetworkMap(config: NodeConfig) {
if (hasNetworkMap()) { if (hasNetworkMap()) {
config.networkMap = networkMapConfig config.networkMap = networkMapConfig
} else { } else {
@ -105,24 +109,24 @@ 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.isDirectory || nodeDir.mkdirs()) { if (nodeDir.forceDirectory()) {
try { try {
// Install any built-in plugins into the working directory.
pluginController.populate(config)
// Ensure that the users have every permission that they need.
config.extendUserPermissions(pluginController.permissionsFor(config))
// 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")
confFile.writeText(config.toText()) 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), overwrite=true)
}
// Execute the Corda node // Execute the Corda node
pty.run(command, System.getenv(), nodeDir.toString()) pty.run(command, System.getenv(), nodeDir.toString())
log.info("Launched node: ${config.legalName}") log.info("Launched node: ${config.legalName}")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
log.severe("Failed to launch Corda:" + e) log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e)
return false return false
} }
} else { } else {
@ -139,11 +143,16 @@ class NodeController : Controller() {
nodes.clear() nodes.clear()
} }
/**
* Add a [NodeConfig] object that has been loaded from a profile.
*/
fun register(config: NodeConfig): Boolean { fun register(config: NodeConfig): Boolean {
if (nodes.putIfAbsent(config.key, config) != null) { if (nodes.putIfAbsent(config.key, config) != null) {
return false return false
} }
updatePort(config)
if ((networkMapConfig == null) && config.isNetworkMap()) { if ((networkMapConfig == null) && config.isNetworkMap()) {
networkMapConfig = config networkMapConfig = config
} }
@ -151,9 +160,32 @@ class NodeController : Controller() {
return true return true
} }
fun relocate(config: NodeConfig) = config.moveTo(baseDir) /**
* Creates a node directory that can host a running instance of Corda.
*/
@Throws(IOException::class)
fun install(config: InstallConfig): NodeConfig {
val installed = config.installTo(baseDir)
private fun baseDirFor(time: Long) = jvm.userHome.resolve("demobench").resolve(localFor(time)) pluginController.userPluginsFor(config).forEach {
val pluginDir = Files.createDirectories(installed.pluginDir)
val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString()))
log.info("Installed: $plugin")
}
if (!config.deleteBaseDir()) {
log.warning("Failed to remove '${config.baseDir}'")
}
return installed
}
private fun updatePort(config: NodeConfig) {
val nextPort = 1 + arrayOf(config.artemisPort, config.webPort, config.h2Port).max() as Int
port.getAndUpdate { Math.max(nextPort, it) }
}
private fun baseDirFor(time: Long): Path = jvm.dataHome.resolve(localFor(time))
private fun localFor(time: Long) = SimpleDateFormat("yyyyMMddHHmmss").format(Date(time)) private fun localFor(time: Long) = SimpleDateFormat("yyyyMMddHHmmss").format(Date(time))
} }

View File

@ -1,13 +1,15 @@
package net.corda.demobench.model package net.corda.demobench.model
import tornadofx.Controller import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.logging.Level
import tornadofx.Controller
class ServiceController : Controller() { class ServiceController(resourceName: String = "/services.conf") : Controller() {
val services: List<String> = loadConf(javaClass.classLoader.getResource("services.conf")) val services: List<String> = loadConf(resources.url(resourceName))
val notaries: List<String> = services.filter { it.startsWith("corda.notary.") }.toList() val notaries: List<String> = services.filter { it.startsWith("corda.notary.") }.toList()
@ -18,16 +20,21 @@ class ServiceController : Controller() {
if (url == null) { if (url == null) {
return emptyList() return emptyList()
} else { } else {
val set = TreeSet<String>() try {
InputStreamReader(url.openStream()).useLines { sq -> val set = TreeSet<String>()
sq.forEach { line -> InputStreamReader(url.openStream()).useLines { sq ->
val service = line.trim() sq.forEach { line ->
set.add(service) val service = line.trim()
set.add(service)
log.info("Supports: $service") log.info("Supports: $service")
}
} }
return set.toList()
} catch (e: IOException) {
log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e)
return emptyList()
} }
return set.toList()
} }
} }

View File

@ -0,0 +1,26 @@
package net.corda.demobench.model
import java.util.*
data class User(val user: String, val password: String, var permissions: List<String>) {
fun extendPermissions(extra: Collection<String>) {
val extended = LinkedHashSet<String>(permissions)
extended.addAll(extra)
permissions = extended.toList()
}
fun toMap() = mapOf(
"user" to user,
"password" to password,
"permissions" to permissions
)
}
@Suppress("UNCHECKED_CAST")
fun toUser(map: Map<String, Any>) = User(
map.getOrElse("user", { "none" }) as String,
map.getOrElse("password", { "none" }) as String,
map.getOrElse("permissions", { emptyList<String>() }) as List<String>
)
fun user(name: String) = User(name, "letmein", listOf("StartFlow.net.corda.flows.CashFlow"))

View File

@ -1,57 +0,0 @@
package net.corda.demobench.model
import net.corda.demobench.loggerFor
import java.util.concurrent.Executors
class WebServer(val webServerController: WebServerController) : AutoCloseable {
private companion object {
val log = loggerFor<WebServer>()
}
private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
val nodeDir = config.nodeDir.toFile()
if (!nodeDir.isDirectory) {
log.warn("Working directory '{}' does not exist.", nodeDir.absolutePath)
onExit(config)
return
}
val p = webServerController.process()
.directory(nodeDir)
.start()
process = p
log.info("Launched Web Server for '{}'", config.legalName)
// Close these streams because no-one is using them.
safeClose(p.outputStream)
safeClose(p.inputStream)
safeClose(p.errorStream)
executor.submit {
val exitValue = p.waitFor()
process = null
log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue)
onExit(config)
}
}
override fun close() {
executor.shutdown()
process?.destroy()
}
private fun safeClose(c: AutoCloseable) {
try {
c.close()
} catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message)
}
}
}

View File

@ -0,0 +1,72 @@
package net.corda.demobench.plugin
import java.io.IOException
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.stream.Stream
import kotlinx.support.jdk8.collections.stream
import kotlinx.support.jdk8.streams.toList
import net.corda.core.node.CordaPluginRegistry
import net.corda.demobench.model.HasPlugins
import net.corda.demobench.model.JVMConfig
import net.corda.demobench.model.NodeConfig
import tornadofx.*
class PluginController : Controller() {
private val jvm by inject<JVMConfig>()
private val pluginDir: Path = jvm.applicationDir.resolve("plugins")
private val bankOfCorda = pluginDir.resolve("bank-of-corda.jar").toFile()
/**
* Install any built-in plugins that this node requires.
*/
@Throws(IOException::class)
fun populate(config: NodeConfig) {
// Nodes cannot issue cash unless they contain the "Bank of Corda" plugin.
if (config.isCashIssuer && bankOfCorda.isFile) {
bankOfCorda.copyTo(config.pluginDir.resolve(bankOfCorda.name).toFile(), overwrite=true)
log.info("Installed 'Bank of Corda' plugin")
}
}
/**
* Generate the set of user permissions that this node's plugins require.
*/
@Throws(IOException::class)
fun permissionsFor(config: HasPlugins): List<String> = walkPlugins(config.pluginDir)
.map { plugin -> classLoaderFor(plugin) }
.flatMap { cl -> cl.use(URLClassLoader::flowsFor).stream() }
.map { flow -> "StartFlow.$flow" }
.toList()
/**
* Generates a stream of a node's non-built-it plugins.
*/
@Throws(IOException::class)
fun userPluginsFor(config: HasPlugins): Stream<Path> = walkPlugins(config.pluginDir)
.filter { bankOfCorda.name != it.fileName.toString() }
private fun walkPlugins(pluginDir: Path): Stream<Path> {
return if (Files.isDirectory(pluginDir))
Files.walk(pluginDir, 1).filter(Path::isPlugin)
else
Stream.empty()
}
private fun classLoaderFor(jarPath: Path) = URLClassLoader(arrayOf(jarPath.toUri().toURL()), javaClass.classLoader)
}
private fun URLClassLoader.flowsFor(): List<String> = ServiceLoader.load(CordaPluginRegistry::class.java, this)
.flatMap { plugin ->
val registry = constructorFor(plugin).newInstance() as CordaPluginRegistry
registry.requiredFlows.keys
}
private fun constructorFor(type: Any) = type.javaClass.constructors.filter { it.parameterCount == 0 }.single()
fun Path.isPlugin(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar")
fun Path.inPluginsDir(): Boolean = (this.parent != null) && this.parent.endsWith("plugins/")

View File

@ -1,38 +1,47 @@
package net.corda.demobench.profile package net.corda.demobench.profile
import com.google.common.net.HostAndPort
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import java.io.File import java.io.File
import java.io.IOException
import java.net.URI import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8 import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.* import java.nio.file.*
import java.util.* import java.util.*
import java.util.function.BiPredicate import java.util.function.BiPredicate
import java.util.logging.Level
import java.util.stream.StreamSupport import java.util.stream.StreamSupport
import javafx.stage.FileChooser import javafx.stage.FileChooser
import javafx.stage.FileChooser.ExtensionFilter import javafx.stage.FileChooser.ExtensionFilter
import kotlinx.support.jdk8.collections.spliterator import kotlinx.support.jdk8.collections.spliterator
import net.corda.demobench.model.* import net.corda.demobench.model.*
import net.corda.demobench.plugin.PluginController
import net.corda.demobench.plugin.inPluginsDir
import net.corda.demobench.plugin.isPlugin
import tornadofx.Controller import tornadofx.Controller
class ProfileController : Controller() { class ProfileController : Controller() {
private val jvm by inject<JVMConfig>() private val jvm by inject<JVMConfig>()
private val baseDir = jvm.userHome.resolve("demobench") private val baseDir: Path = jvm.dataHome
private val nodeController by inject<NodeController>() private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>() private val pluginController by inject<PluginController>()
private val installFactory by inject<InstallFactory>()
private val chooser = FileChooser() private val chooser = FileChooser()
init { init {
chooser.title = "DemoBench Profiles" chooser.title = "DemoBench Profiles"
chooser.initialDirectory = baseDir.toFile() chooser.initialDirectory = baseDir.toFile()
chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.zip)", "*.zip", "*.ZIP")) chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.profile)", "*.profile", "*.PROFILE"))
} }
/**
* Saves the active node configurations into a ZIP file, along with their Cordapps.
*/
@Throws(IOException::class)
fun saveProfile(): Boolean { fun saveProfile(): Boolean {
val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".zip") val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".profile")
log.info("Save profile as: $target") log.info("Saving profile as: $target")
val configs = nodeController.activeNodes val configs = nodeController.activeNodes
@ -40,12 +49,29 @@ class ProfileController : Controller() {
// dialogue has already confirmed that this is OK. // dialogue has already confirmed that this is OK.
target.delete() target.delete()
FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs -> // Write the profile as a ZIP file.
configs.forEach { config -> try {
val nodeDir = Files.createDirectories(fs.getPath(config.key)) FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs ->
val file = Files.write(nodeDir.resolve("node.conf"), config.toText().toByteArray(UTF_8)) configs.forEach { config ->
log.info("Wrote: $file") // Write the configuration file.
val nodeDir = Files.createDirectories(fs.getPath(config.key))
val file = Files.write(nodeDir.resolve("node.conf"), config.toText().toByteArray(UTF_8))
log.info("Wrote: $file")
// Write all of the non-built-in plugins.
val pluginDir = Files.createDirectory(nodeDir.resolve("plugins"))
pluginController.userPluginsFor(config).forEach {
val plugin = Files.copy(it, pluginDir.resolve(it.fileName.toString()))
log.info("Wrote: $plugin")
}
}
} }
log.info("Profile saved.")
} catch (e: IOException) {
log.log(Level.SEVERE, "Failed to save profile '$target': '${e.message}'", e)
target.delete()
throw e
} }
return true return true
@ -55,24 +81,50 @@ class ProfileController : Controller() {
return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target
} }
fun openProfile(): List<NodeConfig>? { /**
* Parses a profile (ZIP) file.
*/
@Throws(IOException::class)
fun openProfile(): List<InstallConfig>? {
val chosen = chooser.showOpenDialog(null) ?: return null val chosen = chooser.showOpenDialog(null) ?: return null
log.info("Selected profile: $chosen") log.info("Selected profile: $chosen")
val configs = LinkedList<NodeConfig>() val configs = LinkedList<InstallConfig>()
FileSystems.newFileSystem(chosen.toPath(), null).use { fs -> FileSystems.newFileSystem(chosen.toPath(), null).use { fs ->
// Identify the nodes first...
StreamSupport.stream(fs.rootDirectories.spliterator(), false) StreamSupport.stream(fs.rootDirectories.spliterator(), false)
.flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() }) } .flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() && attr.isRegularFile }) }
.forEach { file -> .map { file ->
try { try {
// Java seems to "walk" through the ZIP file backwards. val config = installFactory.toInstallConfig(parse(file), baseDir)
// So add new config to the front of the list, so that
// our final list is ordered to match the file.
configs.addFirst(toNodeConfig(parse(file)))
log.info("Loaded: $file") log.info("Loaded: $file")
config
} catch (e: Exception) { } catch (e: Exception) {
log.severe("Failed to parse '$file': ${e.message}") log.log(Level.SEVERE, "Failed to parse '$file': ${e.message}", e)
throw e
}
// Java seems to "walk" through the ZIP file backwards.
// So add new config to the front of the list, so that
// our final list is ordered to match the file.
}.forEach { configs.addFirst(it) }
val nodeIndex = configs.map { it.key to it }.toMap()
// Now extract all of the plugins from the ZIP file,
// and copy them to a temporary location.
StreamSupport.stream(fs.rootDirectories.spliterator(), false)
.flatMap { Files.find(it, 3, BiPredicate { p, attr -> p.inPluginsDir() && p.isPlugin() && attr.isRegularFile }) }
.forEach { plugin ->
val config = nodeIndex[plugin.getName(0).toString()] ?: return@forEach
try {
val pluginDir = Files.createDirectories(config.pluginDir)
Files.copy(plugin, pluginDir.resolve(plugin.fileName.toString()))
log.info("Loaded: $plugin")
} catch (e: Exception) {
log.log(Level.SEVERE, "Failed to extract '$plugin': ${e.message}", e)
configs.forEach { c -> c.deleteBaseDir() }
throw e throw e
} }
} }
@ -81,51 +133,8 @@ class ProfileController : Controller() {
return configs 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"))
} else {
log.info("Node '${nodeConfig.legalName}' is the network map")
}
return nodeConfig
}
private fun parse(path: Path): Config = Files.newBufferedReader(path).use { private fun parse(path: Path): Config = Files.newBufferedReader(path).use {
return ConfigFactory.parseReader(it) return ConfigFactory.parseReader(it)
} }
private fun Config.parsePort(path: String): Int {
val address = this.getString(path)
val port = HostAndPort.fromString(address).port
require(nodeController.isPortValid(port), { "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 -> !it.isNullOrEmpty() }
.map { svc ->
require(svc in services, { "Unknown service '$svc'." } )
svc
}.toList()
}
} }

View File

@ -7,6 +7,7 @@ import com.pty4j.PtyProcess
import net.corda.demobench.loggerFor import net.corda.demobench.loggerFor
import java.awt.* import java.awt.*
import java.io.IOException
import java.nio.charset.StandardCharsets.UTF_8 import java.nio.charset.StandardCharsets.UTF_8
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -39,6 +40,7 @@ class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension,
} }
} }
@Throws(IOException::class)
fun run(args: Array<String>, envs: Map<String, String>, workingDir: String?) { fun run(args: Array<String>, envs: Map<String, String>, workingDir: String?) {
check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" }) check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" })

View File

@ -22,8 +22,7 @@ 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.users[0].getOrElse("user") { "none" } as String, rpcClient.start(config.users[0].user, config.users[0].password)
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

@ -7,7 +7,7 @@ import javafx.scene.control.Button
import javafx.scene.control.MenuItem 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.InstallConfig
import net.corda.demobench.model.NodeController import net.corda.demobench.model.NodeController
import net.corda.demobench.profile.ProfileController import net.corda.demobench.profile.ProfileController
import net.corda.demobench.ui.CloseableTab import net.corda.demobench.ui.CloseableTab
@ -58,10 +58,8 @@ class DemoBenchView : View("Corda Demo Bench") {
private fun configureProfileOpen() = menuOpen.setOnAction { private fun configureProfileOpen() = menuOpen.setOnAction {
try { try {
val profile = profileController.openProfile() val profile = profileController.openProfile() ?: return@setOnAction
if (profile != null) { loadProfile(profile)
loadProfile(profile)
}
} catch (e: Exception) { } catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait() ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
} }
@ -88,13 +86,13 @@ class DemoBenchView : View("Corda Demo Bench") {
return nodeTabView return nodeTabView
} }
private fun loadProfile(nodes: List<NodeConfig>) { private fun loadProfile(nodes: List<InstallConfig>) {
closeAllTabs() closeAllTabs()
nodeController.reset() nodeController.reset()
nodes.forEach { nodes.forEach {
val nodeTabView = createNodeTabView(false) val nodeTabView = createNodeTabView(false)
nodeTabView.launch(nodeController.relocate(it)) nodeTabView.launch(nodeController.install(it))
} }
enableAddNodes() enableAddNodes()

View File

@ -1,14 +1,15 @@
package net.corda.demobench.views package net.corda.demobench.views
import java.nio.file.Path
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.*
import javafx.application.Platform import javafx.application.Platform
import javafx.scene.control.SelectionMode.MULTIPLE import javafx.scene.control.SelectionMode.MULTIPLE
import javafx.scene.input.KeyCode
import javafx.scene.layout.Pane import javafx.scene.layout.Pane
import javafx.stage.FileChooser
import javafx.util.converter.NumberStringConverter import javafx.util.converter.NumberStringConverter
import net.corda.demobench.model.NodeConfig import net.corda.demobench.model.*
import net.corda.demobench.model.NodeController
import net.corda.demobench.model.NodeDataModel
import net.corda.demobench.model.ServiceController
import net.corda.demobench.ui.CloseableTab import net.corda.demobench.ui.CloseableTab
import tornadofx.* import tornadofx.*
@ -16,16 +17,30 @@ 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 val showConfig by param(true)
private companion object : Component() {
const val textWidth = 200.0
const val numberWidth = 100.0
const val maxNameLength = 10
private companion object {
val integerFormat = DecimalFormat() val integerFormat = DecimalFormat()
val notNumber = "[^\\d]".toRegex() val notNumber = "[^\\d]".toRegex()
val jvm by inject<JVMConfig>()
init {
integerFormat.isGroupingUsed = false
}
} }
private val model = NodeDataModel()
private val nodeController by inject<NodeController>() private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>() private val serviceController by inject<ServiceController>()
private val chooser = FileChooser()
private val model = NodeDataModel()
private val cordapps = LinkedList<Path>().observable()
private val availableServices: List<String> = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries
private val nodeTerminalView = find<NodeTerminalView>() private val nodeTerminalView = find<NodeTerminalView>()
private val nodeConfigView = stackpane { private val nodeConfigView = stackpane {
@ -33,6 +48,8 @@ class NodeTabView : Fragment() {
form { form {
fieldset("Configuration") { fieldset("Configuration") {
isFillWidth = false
field("Node Name", op = { nodeNameField() }) field("Node Name", op = { nodeNameField() })
field("Nearest City", op = { nearestCityField() }) field("Nearest City", op = { nearestCityField() })
field("P2P Port", op = { p2pPortField() }) field("P2P Port", op = { p2pPortField() })
@ -40,10 +57,37 @@ class NodeTabView : Fragment() {
field("Database Port", op = { databasePortField() }) field("Database Port", op = { databasePortField() })
} }
fieldset("Services") { hbox {
listview(availableServices.observable()) { styleClass.addAll("node-panel")
selectionModel.selectionMode = MULTIPLE
model.item.extraServices.set(selectionModel.selectedItems) fieldset("Services") {
styleClass.addAll("services-panel")
listview(availableServices.observable()) {
selectionModel.selectionMode = MULTIPLE
model.item.extraServices.set(selectionModel.selectedItems)
}
}
fieldset("Cordapps") {
styleClass.addAll("cordapps-panel")
listview(cordapps) {
setOnKeyPressed { key ->
if ((key.code == KeyCode.DELETE) && !selectionModel.isEmpty) {
cordapps.remove(selectionModel.selectedItem)
}
key.consume()
}
}
button("Add Cordapp") {
setOnAction {
val app = (chooser.showOpenDialog(null) ?: return@setOnAction).toPath()
if (!cordapps.contains(app)) {
cordapps.add(app)
}
}
}
} }
} }
@ -61,12 +105,7 @@ class NodeTabView : Fragment() {
val nodeTab = CloseableTab("New Node", root) val nodeTab = CloseableTab("New Node", root)
private val availableServices: List<String>
get() = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries
init { init {
integerFormat.isGroupingUsed = false
// Ensure that we destroy the terminal along with the tab. // Ensure that we destroy the terminal along with the tab.
nodeTab.setOnCloseRequest { nodeTab.setOnCloseRequest {
nodeTerminalView.destroy() nodeTerminalView.destroy()
@ -78,11 +117,14 @@ class NodeTabView : Fragment() {
model.artemisPort.value = nodeController.nextPort model.artemisPort.value = nodeController.nextPort
model.webPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort
model.h2Port.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort
chooser.title = "Cordapps"
chooser.initialDirectory = jvm.dataHome.toFile()
chooser.extensionFilters.add(FileChooser.ExtensionFilter("Cordapps (*.jar)", "*.jar", "*.JAR"))
} }
private fun Pane.nodeNameField() = textfield(model.legalName) { private fun Pane.nodeNameField() = textfield(model.legalName) {
minWidth = 200.0 minWidth = textWidth
maxWidth = 200.0
validator { validator {
if (it == null) { if (it == null) {
error("Node name is required") error("Node name is required")
@ -92,7 +134,7 @@ class NodeTabView : Fragment() {
error("Node name is required") error("Node name is required")
} else if (nodeController.nameExists(name)) { } else if (nodeController.nameExists(name)) {
error("Node with this name already exists") error("Node with this name already exists")
} else if (name.length > 10) { } else if (name.length > maxNameLength) {
error("Name is too long") error("Name is too long")
} else { } else {
null null
@ -102,8 +144,7 @@ class NodeTabView : Fragment() {
} }
private fun Pane.nearestCityField() = textfield(model.nearestCity) { private fun Pane.nearestCityField() = textfield(model.nearestCity) {
minWidth = 200.0 minWidth = textWidth
maxWidth = 200.0
validator { validator {
if (it == null) { if (it == null) {
error("Nearest city is required") error("Nearest city is required")
@ -116,8 +157,7 @@ class NodeTabView : Fragment() {
} }
private fun Pane.p2pPortField() = textfield(model.artemisPort, NumberStringConverter(integerFormat)) { private fun Pane.p2pPortField() = textfield(model.artemisPort, NumberStringConverter(integerFormat)) {
minWidth = 100.0 minWidth = numberWidth
maxWidth = 100.0
validator { validator {
if ((it == null) || it.isEmpty()) { if ((it == null) || it.isEmpty()) {
error("Port number required") error("Port number required")
@ -139,8 +179,7 @@ class NodeTabView : Fragment() {
} }
private fun Pane.webPortField() = textfield(model.webPort, NumberStringConverter(integerFormat)) { private fun Pane.webPortField() = textfield(model.webPort, NumberStringConverter(integerFormat)) {
minWidth = 100.0 minWidth = numberWidth
maxWidth = 100.0
validator { validator {
if ((it == null) || it.isEmpty()) { if ((it == null) || it.isEmpty()) {
error("Port number required") error("Port number required")
@ -162,8 +201,7 @@ class NodeTabView : Fragment() {
} }
private fun Pane.databasePortField() = textfield(model.h2Port, NumberStringConverter(integerFormat)) { private fun Pane.databasePortField() = textfield(model.h2Port, NumberStringConverter(integerFormat)) {
minWidth = 100.0 minWidth = numberWidth
maxWidth = 100.0
validator { validator {
if ((it == null) || it.isEmpty()) { if ((it == null) || it.isEmpty()) {
error("Port number required") error("Port number required")
@ -192,6 +230,7 @@ class NodeTabView : Fragment() {
val config = nodeController.validate(model.item) val config = nodeController.validate(model.item)
if (config != null) { if (config != null) {
nodeConfigView.isVisible = false nodeConfigView.isVisible = false
config.install(cordapps)
launchNode(config) launchNode(config)
} }
} }

View File

@ -10,10 +10,13 @@ import javafx.scene.control.Button
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import net.corda.demobench.explorer.ExplorerController
import net.corda.demobench.model.* import net.corda.demobench.model.*
import net.corda.demobench.pty.R3Pty import net.corda.demobench.pty.R3Pty
import net.corda.demobench.rpc.NodeRPC import net.corda.demobench.rpc.NodeRPC
import net.corda.demobench.ui.PropertyLabel import net.corda.demobench.ui.PropertyLabel
import net.corda.demobench.web.DBViewer
import net.corda.demobench.web.WebServerController
import tornadofx.Fragment import tornadofx.Fragment
class NodeTerminalView : Fragment() { class NodeTerminalView : Fragment() {

View File

@ -1,11 +1,13 @@
package net.corda.demobench.model package net.corda.demobench.web
import java.sql.SQLException
import java.util.concurrent.Executors
import kotlin.reflect.jvm.jvmName
import net.corda.demobench.loggerFor import net.corda.demobench.loggerFor
import org.h2.Driver
import org.h2.server.web.LocalWebServer import org.h2.server.web.LocalWebServer
import org.h2.tools.Server import org.h2.tools.Server
import org.h2.util.JdbcUtils import org.h2.util.JdbcUtils
import java.util.concurrent.Executors
import kotlin.reflect.jvm.jvmName
class DBViewer : AutoCloseable { class DBViewer : AutoCloseable {
private companion object { private companion object {
@ -35,9 +37,10 @@ class DBViewer : AutoCloseable {
webServer.shutdown() webServer.shutdown()
} }
@Throws(SQLException::class)
fun openBrowser(h2Port: Int) { fun openBrowser(h2Port: Int) {
val conn = JdbcUtils.getConnection( val conn = JdbcUtils.getConnection(
org.h2.Driver::class.jvmName, Driver::class.jvmName,
"jdbc:h2:tcp://localhost:$h2Port/node", "jdbc:h2:tcp://localhost:$h2Port/node",
"sa", "sa",
"" ""

View File

@ -0,0 +1,66 @@
package net.corda.demobench.web
import java.io.IOException
import java.util.concurrent.Executors
import net.corda.demobench.loggerFor
import net.corda.demobench.model.NodeConfig
class WebServer internal constructor(private val webServerController: WebServerController) : AutoCloseable {
private companion object {
val log = loggerFor<WebServer>()
}
private val executor = Executors.newSingleThreadExecutor()
private var process: Process? = null
@Throws(IOException::class)
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
val nodeDir = config.nodeDir.toFile()
if (!nodeDir.isDirectory) {
log.warn("Working directory '{}' does not exist.", nodeDir.absolutePath)
onExit(config)
return
}
try {
val p = webServerController.process()
.directory(nodeDir)
.start()
process = p
log.info("Launched Web Server for '{}'", config.legalName)
// Close these streams because no-one is using them.
safeClose(p.outputStream)
safeClose(p.inputStream)
safeClose(p.errorStream)
executor.submit {
val exitValue = p.waitFor()
process = null
log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue)
onExit(config)
}
} catch (e: IOException) {
log.error("Failed to launch Web Server for '{}': {}", config.legalName, e.message)
onExit(config)
throw e
}
}
override fun close() {
executor.shutdown()
process?.destroy()
}
private fun safeClose(c: AutoCloseable) {
try {
c.close()
} catch (e: Exception) {
log.error("Failed to close stream: '{}'", e.message)
}
}
}

View File

@ -1,5 +1,6 @@
package net.corda.demobench.model package net.corda.demobench.web
import net.corda.demobench.model.JVMConfig
import tornadofx.Controller import tornadofx.Controller
class WebServerController : Controller() { class WebServerController : Controller() {

View File

@ -1,6 +1,7 @@
package org.h2.server.web package org.h2.server.web
import java.sql.Connection import java.sql.Connection
import java.sql.SQLException
class LocalWebServer : WebServer() { class LocalWebServer : WebServer() {
@ -8,6 +9,7 @@ class LocalWebServer : WebServer() {
* Create a new session that will not kill the entire * Create a new session that will not kill the entire
* web server if/when we disconnect it. * web server if/when we disconnect it.
*/ */
@Throws(SQLException::class)
override fun addSession(conn: Connection): String { override fun addSession(conn: Connection): String {
val session = createNewSession("local") val session = createNewSession("local")
session.setConnection(conn) session.setConnection(conn)

View File

@ -1,4 +1,5 @@
/* /*
* https://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html
* https://r3-cev.atlassian.net/wiki/display/RH/Color+Palettes * https://r3-cev.atlassian.net/wiki/display/RH/Color+Palettes
*/ */
@ -21,3 +22,22 @@
-fx-background-radius: 5px; -fx-background-radius: 5px;
-fx-opacity: 80%; -fx-opacity: 80%;
} }
.node-panel {
-fx-spacing: 20px;
}
.services-panel {
-fx-pref-width: 600px;
-fx-spacing: 5px;
}
.cordapps-panel {
-fx-pref-width: 600px;
-fx-spacing: 5px;
}
.list-cell {
-fx-control-inner-background: white;
-fx-control-inner-background-alt: gainsboro;
}

View File

@ -10,20 +10,18 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1"> <VBox xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1">
<MenuBar> <MenuBar useSystemMenuBar="true">
<Menu text="File"> <Menu text="File">
<MenuItem fx:id="menuOpen" text="Open"/> <MenuItem fx:id="menuOpen" text="Open"/>
<MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/> <MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/>
</Menu> </Menu>
</MenuBar> </MenuBar>
<StackPane VBox.vgrow="ALWAYS"> <StackPane VBox.vgrow="ALWAYS">
<children> <TabPane fx:id="nodeTabPane" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0"/>
<TabPane fx:id="nodeTabPane" 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">
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
<StackPane.margin> <StackPane.margin>
<Insets right="5.0" top="5.0" /> <Insets right="5.0" top="5.0" />
</StackPane.margin> </StackPane.margin>
</Button> </Button>
</children> </StackPane>
</StackPane>
</VBox> </VBox>

View File

@ -8,27 +8,19 @@
<?import net.corda.demobench.ui.PropertyLabel?> <?import net.corda.demobench.ui.PropertyLabel?>
<VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1"> <VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1">
<children> <HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header">
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header"> <VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0">
<children> <Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" />
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0"> <PropertyLabel fx:id="p2pPort" name="P2P port: " />
<children> </VBox>
<Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" /> <VBox prefHeight="93.0" prefWidth="267.0">
<PropertyLabel fx:id="p2pPort" name="P2P port: " /> <PropertyLabel fx:id="states" name="States in vault: " />
</children> <PropertyLabel fx:id="transactions" name="Known transactions: " />
</VBox> <PropertyLabel fx:id="balance" name="Balance: " />
<VBox prefHeight="93.0" prefWidth="267.0"> </VBox>
<children> <Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
<PropertyLabel fx:id="states" name="States in vault: " /> <Button fx:id="viewDatabaseButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="View&#10;Database" textAlignment="CENTER" />
<PropertyLabel fx:id="transactions" name="Known transactions: " /> <Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Web Server" textAlignment="CENTER" />
<PropertyLabel fx:id="balance" name="Balance: " /> <Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER" />
</children> </HBox>
</VBox>
<Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
<Button fx:id="viewDatabaseButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="View&#10;Database" textAlignment="CENTER" />
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Web Server" textAlignment="CENTER" />
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER" />
</children>
</HBox>
</children>
</VBox> </VBox>

View File

@ -0,0 +1,33 @@
package net.corda.demobench
import net.corda.demobench.config.LoggingConfig
import net.corda.demobench.model.JVMConfigTest
import net.corda.demobench.model.NodeControllerTest
import net.corda.demobench.model.ServiceControllerTest
import org.junit.BeforeClass
import org.junit.runner.RunWith
import org.junit.runners.Suite
/*
* Declare all test classes that need to configure Java Util Logging.
*/
@RunWith(Suite::class)
@Suite.SuiteClasses(
ServiceControllerTest::class,
NodeControllerTest::class,
JVMConfigTest::class
)
class LoggingTestSuite {
/*
* Workaround for bug in Gradle?
* @see http://issues.gradle.org/browse/GRADLE-2524
*/
companion object {
@BeforeClass
@JvmStatic fun `setup logging`() {
LoggingConfig()
}
}
}

View File

@ -0,0 +1,47 @@
package net.corda.demobench.model
import com.jediterm.terminal.ui.UIUtil
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.*
import org.junit.Test
class JVMConfigTest {
private val jvm = JVMConfig()
@Test
fun `test Java path`() {
assertTrue(Files.isExecutable(jvm.javaPath.onFileSystem()))
}
@Test
fun `test application directory`() {
assertTrue(Files.isDirectory(jvm.applicationDir))
}
@Test
fun `test user home`() {
assertTrue(Files.isDirectory(jvm.userHome))
}
@Test
fun `test command for Jar`() {
val command = jvm.commandFor(Paths.get("testapp.jar"), "arg1", "arg2")
val java = jvm.javaPath
assertEquals(listOf(java.toString(), "-jar", "testapp.jar", "arg1", "arg2"), command)
}
@Test
fun `test process for Jar`() {
val process = jvm.processFor(Paths.get("testapp.jar"), "arg1", "arg2", "arg3")
val java = jvm.javaPath
assertEquals(listOf(java.toString(), "-jar", "testapp.jar", "arg1", "arg2", "arg3"), process.command())
}
private fun Path.onFileSystem(): Path
= if (UIUtil.isWindows) this.parent.resolve(Paths.get(this.fileName.toString() + ".exe"))
else this
}

View File

@ -0,0 +1,19 @@
package net.corda.demobench.model
import kotlin.test.*
import org.junit.Test
class NetworkMapConfigTest {
@Test
fun keyValue() {
val config = NetworkMapConfig("My\tNasty Little\rLabel\n", 10000)
assertEquals("mynastylittlelabel", config.key)
}
@Test
fun removeWhitespace() {
assertEquals("OneTwoThreeFour!", "One\tTwo \rThree\r\nFour!".stripWhitespace())
}
}

View File

@ -0,0 +1,201 @@
package net.corda.demobench.model
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.*
import org.junit.Test
class NodeConfigTest {
private val baseDir: Path = Paths.get(".").toAbsolutePath()
@Test
fun `test name`() {
val config = createConfig(legalName = "My Name")
assertEquals("My Name", config.legalName)
assertEquals("myname", config.key)
}
@Test
fun `test node directory`() {
val config = createConfig(legalName = "My Name")
assertEquals(baseDir.resolve("myname"), config.nodeDir)
}
@Test
fun `test explorer directory`() {
val config = createConfig(legalName = "My Name")
assertEquals(baseDir.resolve("myname-explorer"), config.explorerDir)
}
@Test
fun `test plugin directory`() {
val config = createConfig(legalName = "My Name")
assertEquals(baseDir.resolve("myname").resolve("plugins"), config.pluginDir)
}
@Test
fun `test nearest city`() {
val config = createConfig(nearestCity = "Leicester")
assertEquals("Leicester", config.nearestCity)
}
@Test
fun `test artemis port`() {
val config = createConfig(artemisPort = 10001)
assertEquals(10001, config.artemisPort)
}
@Test
fun `test web port`() {
val config = createConfig(webPort = 20001)
assertEquals(20001, config.webPort)
}
@Test
fun `test H2 port`() {
val config = createConfig(h2Port = 30001)
assertEquals(30001, config.h2Port)
}
@Test
fun `test services`() {
val config = createConfig(services = listOf("my.service"))
assertEquals(listOf("my.service"), config.extraServices)
}
@Test
fun `test users`() {
val config = createConfig(users = listOf(user("myuser")))
assertEquals(listOf(user("myuser")), config.users)
}
@Test
fun `test default state`() {
val config = createConfig()
assertEquals(NodeState.STARTING, config.state)
}
@Test
fun `test network map`() {
val config = createConfig()
assertNull(config.networkMap)
assertTrue(config.isNetworkMap())
}
@Test
fun `test cash issuer`() {
val config = createConfig(services = listOf("corda.issuer.GBP"))
assertTrue(config.isCashIssuer)
}
@Test
fun `test not cash issuer`() {
val config = createConfig(services = listOf("corda.issuerubbish"))
assertFalse(config.isCashIssuer)
}
@Test
fun `test SSL configuration`() {
val config = createConfig(legalName = "My Name")
val ssl = config.ssl
assertEquals(baseDir.resolve("myname").resolve("certificates"), ssl.certificatesDirectory)
assertEquals("cordacadevpass", ssl.keyStorePassword)
assertEquals("trustpass", ssl.trustStorePassword)
}
@Test
fun `test config text`() {
val config = createConfig(
legalName = "My Name",
nearestCity = "Stockholm",
artemisPort = 10001,
webPort = 20001,
h2Port = 30001,
services = listOf("my.service"),
users = listOf(user("jenny"))
)
assertEquals("{"
+ "\"artemisAddress\":\"localhost:10001\","
+ "\"extraAdvertisedServiceIds\":\"my.service\","
+ "\"h2port\":30001,"
+ "\"myLegalName\":\"MyName\","
+ "\"nearestCity\":\"Stockholm\","
+ "\"rpcUsers\":["
+ "{\"password\":\"letmein\",\"permissions\":[\"StartFlow.net.corda.flows.CashFlow\"],\"user\":\"jenny\"}"
+ "],"
+ "\"useTestClock\":true,"
+ "\"webAddress\":\"localhost:20001\""
+ "}", config.toText().stripWhitespace())
}
@Test
fun `test config text with network map`() {
val config = createConfig(
legalName = "My Name",
nearestCity = "Stockholm",
artemisPort = 10001,
webPort = 20001,
h2Port = 30001,
services = listOf("my.service"),
users = listOf(user("jenny"))
)
config.networkMap = NetworkMapConfig("Notary", 12345)
assertEquals("{"
+ "\"artemisAddress\":\"localhost:10001\","
+ "\"extraAdvertisedServiceIds\":\"my.service\","
+ "\"h2port\":30001,"
+ "\"myLegalName\":\"MyName\","
+ "\"nearestCity\":\"Stockholm\","
+ "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"Notary\"},"
+ "\"rpcUsers\":["
+ "{\"password\":\"letmein\",\"permissions\":[\"StartFlow.net.corda.flows.CashFlow\"],\"user\":\"jenny\"}"
+ "],"
+ "\"useTestClock\":true,"
+ "\"webAddress\":\"localhost:20001\""
+ "}", config.toText().stripWhitespace())
}
@Test
fun `test moving`() {
val config = createConfig(legalName = "My Name")
val elsewhere = baseDir.resolve("elsewhere")
val moved = config.moveTo(elsewhere)
assertEquals(elsewhere.resolve("myname"), moved.nodeDir)
assertEquals(elsewhere.resolve("myname-explorer"), moved.explorerDir)
assertEquals(elsewhere.resolve("myname").resolve("plugins"), moved.pluginDir)
assertEquals(elsewhere.resolve("myname").resolve("certificates"), moved.ssl.certificatesDirectory)
}
@Test
fun `test adding user permissions`() {
val config = createConfig(users = listOf(user("brian"), user("stewie")))
config.extendUserPermissions(listOf("MyFlow.pluginFlow", "MyFlow.otherFlow"))
config.users.forEach {
assertEquals(listOf("StartFlow.net.corda.flows.CashFlow", "MyFlow.pluginFlow", "MyFlow.otherFlow"), it.permissions)
}
}
private fun createConfig(
legalName: String = "Unknown",
nearestCity: String = "Nowhere",
artemisPort: Int = -1,
webPort: Int = -1,
h2Port: Int = -1,
services: List<String> = listOf("extra.service"),
users: List<User> = listOf(user("guest"))
) = NodeConfig(
baseDir,
legalName = legalName,
nearestCity = nearestCity,
artemisPort = artemisPort,
webPort = webPort,
h2Port = h2Port,
extraServices = services,
users = users
)
}

View File

@ -0,0 +1,169 @@
package net.corda.demobench.model
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.*
import org.junit.Test
class NodeControllerTest {
private val baseDir: Path = Paths.get(".").toAbsolutePath()
private val controller = NodeController()
@Test
fun `test unique nodes after validate`() {
val data = NodeData()
data.legalName.value = "Node 1"
assertNotNull(controller.validate(data))
assertNull(controller.validate(data))
}
@Test
fun `test unique key after validate`() {
val data = NodeData()
data.legalName.value = "Node 1"
assertFalse(controller.keyExists("node1"))
controller.validate(data)
assertTrue(controller.keyExists("node1"))
}
@Test
fun `test matching name after validate`() {
val data = NodeData()
data.legalName.value = "Node 1"
assertFalse(controller.nameExists("Node 1"))
assertFalse(controller.nameExists("Node1"))
assertFalse(controller.nameExists("node 1"))
controller.validate(data)
assertTrue(controller.nameExists("Node 1"))
assertTrue(controller.nameExists("Node1"))
assertTrue(controller.nameExists("node 1"))
}
@Test
fun `test first validated node becomes network map`() {
val data = NodeData()
data.legalName.value = "Node 1"
data.artemisPort.value = 100000
assertFalse(controller.hasNetworkMap())
controller.validate(data)
assertTrue(controller.hasNetworkMap())
}
@Test
fun `test register unique nodes`() {
val config = createConfig(legalName = "Node 2")
assertTrue(controller.register(config))
assertFalse(controller.register(config))
}
@Test
fun `test unique key after register`() {
val config = createConfig(legalName = "Node 2")
assertFalse(controller.keyExists("node2"))
controller.register(config)
assertTrue(controller.keyExists("node2"))
}
@Test
fun `test matching name after register`() {
val config = createConfig(legalName = "Node 2")
assertFalse(controller.nameExists("Node 2"))
assertFalse(controller.nameExists("Node2"))
assertFalse(controller.nameExists("node 2"))
controller.register(config)
assertTrue(controller.nameExists("Node 2"))
assertTrue(controller.nameExists("Node2"))
assertTrue(controller.nameExists("node 2"))
}
@Test
fun `test register network map node`() {
val config = createConfig(legalName = "Node is Network Map")
assertTrue(config.isNetworkMap())
assertFalse(controller.hasNetworkMap())
controller.register(config)
assertTrue(controller.hasNetworkMap())
}
@Test
fun `test register non-network-map node`() {
val config = createConfig(legalName = "Node is not Network Map")
config.networkMap = NetworkMapConfig("Notary", 10000)
assertFalse(config.isNetworkMap())
assertFalse(controller.hasNetworkMap())
controller.register(config)
assertFalse(controller.hasNetworkMap())
}
@Test
fun `test valid ports`() {
assertFalse(controller.isPortValid(NodeController.minPort - 1))
assertTrue(controller.isPortValid(NodeController.minPort))
assertTrue(controller.isPortValid(NodeController.maxPort))
assertFalse(controller.isPortValid(NodeController.maxPort + 1))
}
@Test
fun `test artemis port is max`() {
val config = createConfig(artemisPort = NodeController.firstPort + 1234)
assertEquals(NodeController.firstPort, controller.nextPort)
controller.register(config)
assertEquals(NodeController.firstPort + 1235, controller.nextPort)
}
@Test
fun `test web port is max`() {
val config = createConfig(webPort = NodeController.firstPort + 2356)
assertEquals(NodeController.firstPort, controller.nextPort)
controller.register(config)
assertEquals(NodeController.firstPort + 2357, controller.nextPort)
}
@Test
fun `test H2 port is max`() {
val config = createConfig(h2Port = NodeController.firstPort + 3478)
assertEquals(NodeController.firstPort, controller.nextPort)
controller.register(config)
assertEquals(NodeController.firstPort + 3479, controller.nextPort)
}
@Test
fun `dispose node`() {
val config = createConfig(legalName = "MyName")
controller.register(config)
assertEquals(NodeState.STARTING, config.state)
assertTrue(controller.keyExists("myname"))
controller.dispose(config)
assertEquals(NodeState.DEAD, config.state)
assertTrue(controller.keyExists("myname"))
}
private fun createConfig(
legalName: String = "Unknown",
nearestCity: String = "Nowhere",
artemisPort: Int = -1,
webPort: Int = -1,
h2Port: Int = -1,
services: List<String> = listOf("extra.service"),
users: List<User> = listOf(user("guest"))
) = NodeConfig(
baseDir,
legalName = legalName,
nearestCity = nearestCity,
artemisPort = artemisPort,
webPort = webPort,
h2Port = h2Port,
extraServices = services,
users = users
)
}

View File

@ -0,0 +1,42 @@
package net.corda.demobench.model
import kotlin.test.*
import org.junit.Test
class ServiceControllerTest {
@Test
fun `test empty`() {
val controller = ServiceController("/empty-services.conf")
assertNotNull(controller.services)
assertTrue(controller.services.isEmpty())
assertNotNull(controller.notaries)
assertTrue(controller.notaries.isEmpty())
}
@Test
fun `test duplicates`() {
val controller = ServiceController("/duplicate-services.conf")
assertNotNull(controller.services)
assertEquals(listOf("corda.example"), controller.services)
}
@Test
fun `test notaries`() {
val controller = ServiceController("/notary-services.conf")
assertNotNull(controller.notaries)
assertEquals(listOf("corda.notary.simple"), controller.notaries)
}
@Test
fun `test services`() {
val controller = ServiceController()
assertNotNull(controller.services)
assertTrue(controller.services.isNotEmpty())
assertNotNull(controller.notaries)
assertTrue(controller.notaries.isNotEmpty())
}
}

View File

@ -0,0 +1,53 @@
package net.corda.demobench.model
import org.junit.Test
import kotlin.test.*
class UserTest {
@Test
fun createFromEmptyMap() {
val user = toUser(emptyMap())
assertEquals("none", user.user)
assertEquals("none", user.password)
assertEquals(emptyList<String>(), user.permissions)
}
@Test
fun createFromMap() {
val map = mapOf(
"user" to "MyName",
"password" to "MyPassword",
"permissions" to listOf("Flow.MyFlow")
)
val user = toUser(map)
assertEquals("MyName", user.user)
assertEquals("MyPassword", user.password)
assertEquals(listOf("Flow.MyFlow"), user.permissions)
}
@Test
fun userToMap() {
val user = User("MyName", "MyPassword", listOf("Flow.MyFlow"))
val map = user.toMap()
assertEquals("MyName", map["user"])
assertEquals("MyPassword", map["password"])
assertEquals(listOf("Flow.MyFlow"), map["permissions"])
}
@Test
fun `adding extra permissions`() {
val user = User("MyName", "MyPassword", listOf("Flow.MyFlow"))
user.extendPermissions(listOf("Flow.MyFlow", "Flow.MyNewFlow"))
assertEquals(listOf("Flow.MyFlow", "Flow.MyNewFlow"), user.permissions)
}
@Test
fun `default user`() {
val user = user("guest")
assertEquals("guest", user.user)
assertEquals("letmein", user.password)
assertEquals(listOf("StartFlow.net.corda.flows.CashFlow"), user.permissions)
}
}

View File

@ -0,0 +1,3 @@
corda.example
corda.example
corda.example

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %-5level %c{1} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -0,0 +1,2 @@
corda.notary.simple
corda.example