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.pkg_source = "$buildDir/packagesrc"
ext.pkg_outDir = "$buildDir/javapackage"
ext.dist_source = "$pkg_source/demobench-$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 {
main() {
contents {
@ -134,7 +139,7 @@ distributions {
task javapackage(dependsOn: 'distZip') {
doLast {
delete([pkg_source, "$buildDir/exedir"])
delete([pkg_source, pkg_outDir])
copy {
from(zipTree(distZip.outputs.files.singleFile))
@ -175,7 +180,7 @@ task javapackage(dependsOn: 'distZip') {
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)
info(title: 'DemoBench', vendor: 'R3', description: 'A sales and educational tool for demonstrating Corda.')
resources {

View File

@ -7,4 +7,7 @@ if [ -z "$JAVA_HOME" -o ! -x $JAVA_HOME/bin/java ]; then
exit 1
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
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
:NoJavaHome
echo "Please set JAVA_HOME correctly"
@echo "Please set JAVA_HOME correctly"
: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
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() {
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 applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath()
@ -14,13 +15,12 @@ class JVMConfig : Controller() {
log.info("Java executable: $javaPath")
}
fun commandFor(jarPath: Path, vararg args: String): Array<String> {
return arrayOf(javaPath.toString(), "-jar", jarPath.toString(), *args)
fun commandFor(jarPath: Path, vararg args: String): List<String> {
return listOf(javaPath.toString(), "-jar", jarPath.toString(), *args)
}
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) {
private var keyValue = toKey(legalName)
val key: String get() = keyValue
val key: String = legalName.toKey()
}
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 java.lang.String.join
import java.io.File
import java.nio.file.Path
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import net.corda.node.services.config.SSLConfiguration
class NodeConfig(
@ -13,24 +16,16 @@ class NodeConfig(
val webPort: Int,
val h2Port: Int,
val extraServices: List<String>,
val users: List<Map<String, Any>> = listOf(defaultUser),
val users: List<User> = listOf(user("guest")),
var networkMap: NetworkMapConfig? = null
) : NetworkMapConfig(legalName, artemisPort) {
) : NetworkMapConfig(legalName, artemisPort), HasPlugins {
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)
override val pluginDir: Path = nodeDir.resolve("plugins")
val explorerDir: Path = baseDir.resolve("$key-explorer")
val ssl: SSLConfiguration = object : SSLConfiguration {
@ -61,7 +56,7 @@ class NodeConfig(
.withValue("legalName", valueFor(n.legalName))
} ))
.withValue("webAddress", addressValueFor(webPort))
.withValue("rpcUsers", valueFor(users))
.withValue("rpcUsers", valueFor(users.map(User::toMap).toList()))
.withValue("h2port", valueFor(h2Port))
.withValue("useTestClock", valueFor(true))
@ -70,13 +65,25 @@ class NodeConfig(
fun moveTo(baseDir: Path) = NodeConfig(
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 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()
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.lang.management.ManagementFactory
import java.net.ServerSocket
import java.nio.file.Files
import java.nio.file.Path
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import net.corda.demobench.plugin.PluginController
import net.corda.demobench.pty.R3Pty
import tornadofx.Controller
class NodeController : Controller() {
private companion object {
companion object {
const val firstPort = 10000
const val minPort = 1024
const val maxPort = 65535
}
private val jvm by inject<JVMConfig>()
private val pluginController by inject<PluginController>()
private var baseDir = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
private val pluginDir = jvm.applicationDir.resolve("plugins")
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 var baseDir: Path = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
private val cordaPath: Path = jvm.applicationDir.resolve("corda").resolve("corda.jar")
private val command = jvm.commandFor(cordaPath).toTypedArray()
private val nodes = LinkedHashMap<String, NodeConfig>()
private val port = AtomicInteger(firstPort)
@ -40,6 +41,9 @@ class NodeController : Controller() {
log.info("Corda JAR: $cordaPath")
}
/**
* Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView].
*/
fun validate(nodeData: NodeData): NodeConfig? {
val config = NodeConfig(
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 nameExists(name: String) = keyExists(toKey(name))
fun nameExists(name: String) = keyExists(name.toKey())
fun hasNetworkMap(): Boolean = networkMapConfig != null
fun chooseNetworkMap(config: NodeConfig) {
private fun chooseNetworkMap(config: NodeConfig) {
if (hasNetworkMap()) {
config.networkMap = networkMapConfig
} else {
@ -105,24 +109,24 @@ class NodeController : Controller() {
fun runCorda(pty: R3Pty, config: NodeConfig): Boolean {
val nodeDir = config.nodeDir.toFile()
if (nodeDir.isDirectory || nodeDir.mkdirs()) {
if (nodeDir.forceDirectory()) {
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.
val confFile = nodeDir.resolve("node.conf")
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
pty.run(command, System.getenv(), nodeDir.toString())
log.info("Launched node: ${config.legalName}")
return true
} catch (e: Exception) {
log.severe("Failed to launch Corda:" + e)
log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", e)
return false
}
} else {
@ -139,11 +143,16 @@ class NodeController : Controller() {
nodes.clear()
}
/**
* Add a [NodeConfig] object that has been loaded from a profile.
*/
fun register(config: NodeConfig): Boolean {
if (nodes.putIfAbsent(config.key, config) != null) {
return false
}
updatePort(config)
if ((networkMapConfig == null) && config.isNetworkMap()) {
networkMapConfig = config
}
@ -151,9 +160,32 @@ class NodeController : Controller() {
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))
}

View File

@ -1,13 +1,15 @@
package net.corda.demobench.model
import tornadofx.Controller
import java.io.IOException
import java.io.InputStreamReader
import java.net.URL
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()
@ -18,6 +20,7 @@ class ServiceController : Controller() {
if (url == null) {
return emptyList()
} else {
try {
val set = TreeSet<String>()
InputStreamReader(url.openStream()).useLines { sq ->
sq.forEach { line ->
@ -28,6 +31,10 @@ class ServiceController : Controller() {
}
}
return set.toList()
} catch (e: IOException) {
log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e)
return emptyList()
}
}
}

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
import com.google.common.net.HostAndPort
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import java.io.File
import java.io.IOException
import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.*
import java.util.*
import java.util.function.BiPredicate
import java.util.logging.Level
import java.util.stream.StreamSupport
import javafx.stage.FileChooser
import javafx.stage.FileChooser.ExtensionFilter
import kotlinx.support.jdk8.collections.spliterator
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
class ProfileController : Controller() {
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 serviceController by inject<ServiceController>()
private val pluginController by inject<PluginController>()
private val installFactory by inject<InstallFactory>()
private val chooser = FileChooser()
init {
chooser.title = "DemoBench Profiles"
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 {
val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".zip")
log.info("Save profile as: $target")
val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".profile")
log.info("Saving profile as: $target")
val configs = nodeController.activeNodes
@ -40,13 +49,30 @@ class ProfileController : Controller() {
// dialogue has already confirmed that this is OK.
target.delete()
// Write the profile as a ZIP file.
try {
FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs ->
configs.forEach { config ->
// 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
}
@ -55,24 +81,50 @@ class ProfileController : Controller() {
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
log.info("Selected profile: $chosen")
val configs = LinkedList<NodeConfig>()
val configs = LinkedList<InstallConfig>()
FileSystems.newFileSystem(chosen.toPath(), null).use { fs ->
// Identify the nodes first...
StreamSupport.stream(fs.rootDirectories.spliterator(), false)
.flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() }) }
.forEach { file ->
.flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() && attr.isRegularFile }) }
.map { file ->
try {
val config = installFactory.toInstallConfig(parse(file), baseDir)
log.info("Loaded: $file")
config
} catch (e: Exception) {
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.
configs.addFirst(toNodeConfig(parse(file)))
log.info("Loaded: $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.severe("Failed to parse '$file': ${e.message}")
log.log(Level.SEVERE, "Failed to extract '$plugin': ${e.message}", e)
configs.forEach { c -> c.deleteBaseDir() }
throw e
}
}
@ -81,51 +133,8 @@ class ProfileController : Controller() {
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 {
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 java.awt.*
import java.io.IOException
import java.nio.charset.StandardCharsets.UTF_8
import java.util.*
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?) {
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() {
override fun run() {
try {
rpcClient.start(config.users[0].getOrElse("user") { "none" } as String,
config.users[0].getOrElse("password") { "none" } as String)
rpcClient.start(config.users[0].user, config.users[0].password)
val ops = rpcClient.proxy()
// 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.Tab
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.profile.ProfileController
import net.corda.demobench.ui.CloseableTab
@ -58,10 +58,8 @@ class DemoBenchView : View("Corda Demo Bench") {
private fun configureProfileOpen() = menuOpen.setOnAction {
try {
val profile = profileController.openProfile()
if (profile != null) {
val profile = profileController.openProfile() ?: return@setOnAction
loadProfile(profile)
}
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
}
@ -88,13 +86,13 @@ class DemoBenchView : View("Corda Demo Bench") {
return nodeTabView
}
private fun loadProfile(nodes: List<NodeConfig>) {
private fun loadProfile(nodes: List<InstallConfig>) {
closeAllTabs()
nodeController.reset()
nodes.forEach {
val nodeTabView = createNodeTabView(false)
nodeTabView.launch(nodeController.relocate(it))
nodeTabView.launch(nodeController.install(it))
}
enableAddNodes()

View File

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

View File

@ -10,10 +10,13 @@ import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.layout.VBox
import javax.swing.SwingUtilities
import net.corda.demobench.explorer.ExplorerController
import net.corda.demobench.model.*
import net.corda.demobench.pty.R3Pty
import net.corda.demobench.rpc.NodeRPC
import net.corda.demobench.ui.PropertyLabel
import net.corda.demobench.web.DBViewer
import net.corda.demobench.web.WebServerController
import tornadofx.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 org.h2.Driver
import org.h2.server.web.LocalWebServer
import org.h2.tools.Server
import org.h2.util.JdbcUtils
import java.util.concurrent.Executors
import kotlin.reflect.jvm.jvmName
class DBViewer : AutoCloseable {
private companion object {
@ -35,9 +37,10 @@ class DBViewer : AutoCloseable {
webServer.shutdown()
}
@Throws(SQLException::class)
fun openBrowser(h2Port: Int) {
val conn = JdbcUtils.getConnection(
org.h2.Driver::class.jvmName,
Driver::class.jvmName,
"jdbc:h2:tcp://localhost:$h2Port/node",
"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
class WebServerController : Controller() {

View File

@ -1,6 +1,7 @@
package org.h2.server.web
import java.sql.Connection
import java.sql.SQLException
class LocalWebServer : WebServer() {
@ -8,6 +9,7 @@ class LocalWebServer : WebServer() {
* Create a new session that will not kill the entire
* web server if/when we disconnect it.
*/
@Throws(SQLException::class)
override fun addSession(conn: Connection): String {
val session = createNewSession("local")
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
*/
@ -21,3 +22,22 @@
-fx-background-radius: 5px;
-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?>
<VBox xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1">
<MenuBar>
<MenuBar useSystemMenuBar="true">
<Menu text="File">
<MenuItem fx:id="menuOpen" text="Open"/>
<MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/>
</Menu>
</MenuBar>
<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"/>
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
<StackPane.margin>
<Insets right="5.0" top="5.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
</VBox>

View File

@ -8,27 +8,19 @@
<?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">
<children>
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header">
<children>
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0">
<children>
<Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" />
<PropertyLabel fx:id="p2pPort" name="P2P port: " />
</children>
</VBox>
<VBox prefHeight="93.0" prefWidth="267.0">
<children>
<PropertyLabel fx:id="states" name="States in vault: " />
<PropertyLabel fx:id="transactions" name="Known transactions: " />
<PropertyLabel fx:id="balance" name="Balance: " />
</children>
</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>

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