mirror of
https://github.com/corda/corda.git
synced 2025-01-30 08:04:16 +00:00
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:
parent
d52accb52c
commit
c64ab4b7a5
@ -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 {
|
||||
|
@ -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
|
@ -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
|
13
tools/demobench/package-demobench-rpm.sh
Executable file
13
tools/demobench/package-demobench-rpm.sh
Executable 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
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
interface HasPlugins {
|
||||
val pluginDir: Path
|
||||
}
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
||||
}
|
||||
|
@ -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,16 +20,21 @@ class ServiceController : Controller() {
|
||||
if (url == null) {
|
||||
return emptyList()
|
||||
} else {
|
||||
val set = TreeSet<String>()
|
||||
InputStreamReader(url.openStream()).useLines { sq ->
|
||||
sq.forEach { line ->
|
||||
val service = line.trim()
|
||||
set.add(service)
|
||||
try {
|
||||
val set = TreeSet<String>()
|
||||
InputStreamReader(url.openStream()).useLines { sq ->
|
||||
sq.forEach { line ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"))
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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/")
|
@ -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,12 +49,29 @@ class ProfileController : Controller() {
|
||||
// dialogue has already confirmed that this is OK.
|
||||
target.delete()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs ->
|
||||
configs.forEach { config ->
|
||||
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 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 {
|
||||
// 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)))
|
||||
val config = installFactory.toInstallConfig(parse(file), baseDir)
|
||||
log.info("Loaded: $file")
|
||||
config
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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" })
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
loadProfile(profile)
|
||||
}
|
||||
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()
|
||||
|
@ -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,10 +57,37 @@ class NodeTabView : Fragment() {
|
||||
field("Database Port", op = { databasePortField() })
|
||||
}
|
||||
|
||||
fieldset("Services") {
|
||||
listview(availableServices.observable()) {
|
||||
selectionModel.selectionMode = MULTIPLE
|
||||
model.item.extraServices.set(selectionModel.selectedItems)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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",
|
||||
""
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
<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">
|
||||
<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">
|
||||
<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" />
|
||||
<Insets right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</Button>
|
||||
</children>
|
||||
</StackPane>
|
||||
</Button>
|
||||
</StackPane>
|
||||
</VBox>
|
||||
|
@ -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 Database" textAlignment="CENTER" />
|
||||
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Web Server" textAlignment="CENTER" />
|
||||
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Explorer" textAlignment="CENTER" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header">
|
||||
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0">
|
||||
<Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" />
|
||||
<PropertyLabel fx:id="p2pPort" name="P2P port: " />
|
||||
</VBox>
|
||||
<VBox prefHeight="93.0" prefWidth="267.0">
|
||||
<PropertyLabel fx:id="states" name="States in vault: " />
|
||||
<PropertyLabel fx:id="transactions" name="Known transactions: " />
|
||||
<PropertyLabel fx:id="balance" name="Balance: " />
|
||||
</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 Database" textAlignment="CENTER" />
|
||||
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Web Server" textAlignment="CENTER" />
|
||||
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Explorer" textAlignment="CENTER" />
|
||||
</HBox>
|
||||
</VBox>
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
corda.example
|
||||
corda.example
|
||||
corda.example
|
14
tools/demobench/src/test/resources/logback-test.xml
Normal file
14
tools/demobench/src/test/resources/logback-test.xml
Normal 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>
|
2
tools/demobench/src/test/resources/notary-services.conf
Normal file
2
tools/demobench/src/test/resources/notary-services.conf
Normal file
@ -0,0 +1,2 @@
|
||||
corda.notary.simple
|
||||
corda.example
|
Loading…
x
Reference in New Issue
Block a user