Merged in demobench (pull request #16)

Demobench

Approved-by: Clinton Alexander
Approved-by: Shams Asari
This commit is contained in:
Chris Rankin 2017-02-24 09:43:10 +00:00
commit 438fc97881
69 changed files with 2129 additions and 108 deletions

View File

@ -29,6 +29,7 @@ buildscript {
ext.jopt_simple_version = '5.0.2'
ext.jansi_version = '1.14'
ext.hibernate_version = '5.2.6.Final'
ext.h2_version = '1.4.193'
ext.dokka_version = '0.9.13'
repositories {

View File

@ -11,3 +11,11 @@ dataSourceProperties {
"dataSource.password" = ""
}
h2port = 0
jiraConfig{
address = "https://doorman-jira-host/"
projectCode = "TD"
username = "username"
password = "password"
doneTransitionCode = 41
}

View File

@ -130,7 +130,7 @@ dependencies {
testCompile project(':core')
// For H2 database support in persistence
compile "com.h2database:h2:1.4.193"
compile "com.h2database:h2:$h2_version"
// Exposed: Kotlin SQL library - under evaluation
// TODO: Upgrade to Exposed 0.7 (has API changes)

10
package-demobench-dmg.sh Executable file
View File

@ -0,0 +1,10 @@
#!/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
exec $DIRNAME/gradlew -PpackageType=dmg javapackage $*

17
package-demobench-exe.bat Normal file
View File

@ -0,0 +1,17 @@
@echo off
@rem Creates an EXE installer for DemoBench.
@rem Assumes that Inno Setup 5+ has already been installed (http://www.jrsoftware.org/isinfo.php)
if not defined JAVA_HOME goto NoJavaHome
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
call %DIRNAME%\gradlew -PpackageType=exe javapackage
goto end
:NoJavaHome
echo "Please set JAVA_HOME correctly"
:end

View File

@ -11,8 +11,9 @@ include 'doorman'
include 'experimental'
include 'experimental:sandbox'
include 'test-utils'
include 'tools:demobench'
include 'tools:explorer'
include 'tools:explorer:capsule'
include 'tools:demobench'
include 'tools:loadtest'
include 'docs/source/example-code' // Note that we are deliberately choosing to use '/' here. With ':' gradle would treat the directories as actual projects.
include 'samples:attachment-demo'

View File

@ -1,10 +1,20 @@
group 'net.corda'
version '0.7-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.0.6'
ext.tornadofx_version = '1.6.2'
ext.jna_version = '4.1.0'
ext.purejavacomm_version = '0.0.17'
ext.guava_version = '14.0.1'
ext.slf4j_version = '1.7.22'
ext.logback_version = '1.1.10'
ext.controlsfx_version = '8.40.12'
ext.java_home = System.properties.'java.home'
ext.pkg_source = "$buildDir/packagesrc"
ext.dist_source = "$pkg_source/demobench-$version"
ext.pkg_version = "$version".indexOf('-') >= 0 ? "$version".substring(0, "$version".indexOf('-')) : version
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
@ -14,17 +24,185 @@ buildscript {
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'application'
evaluationDependsOn(':tools:explorer:capsule')
sourceCompatibility = 1.8
mainClassName = 'net.corda.demobench.DemoBench'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig']
repositories {
flatDir {
dirs 'libs'
}
mavenLocal()
mavenCentral()
jcenter()
maven {
url 'http://www.sparetimelabs.com/maven2'
}
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testCompile group: 'junit', name: 'junit', version: '4.11'
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
compile 'no.tornado:tornadofx:1.5.7'
compile "no.tornado:tornadofx:$tornadofx_version"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
compile "org.controlsfx:controlsfx:$controlsfx_version"
// ONLY USING THE RPC CLIENT!?
compile project(':node')
compile "com.h2database:h2:$h2_version"
compile "net.java.dev.jna:jna:$jna_version"
compile "net.java.dev.jna:jna-platform:$jna_version"
compile "com.google.guava:guava:$guava_version"
compile "com.sparetimelabs:purejavacomm:$purejavacomm_version"
compile "org.apache.logging.log4j:log4j-to-slf4j:$log4j_version"
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "ch.qos.logback:logback-classic:$logback_version"
compile "com.typesafe:config:$typesafe_config_version"
// These libraries don't exist in any Maven repository I can find.
// See: https://github.com/JetBrains/jediterm
//
// The terminal JAR here has also been tweaked:
// See: https://github.com/JetBrains/jediterm/issues/144
compile ':jediterm-terminal-2.5'
compile ':pty4j-0.7.2'
testCompile group: 'junit', name: 'junit', version: junit_version
}
// We don't want the Node to drag these transitive dependencies in either!
configurations.all {
exclude module: 'commons-logging'
exclude module: 'log4j-slf4j-impl'
exclude module: 'log4j-core'
}
jar {
manifest {
attributes(
'Corda-Version': corda_version,
'Main-Class': mainClassName,
'Class-Path': configurations.compile.collect { it.getName() }.join(' ')
)
}
}
distributions {
main() {
contents {
into('lib/linux') {
from 'libs/linux'
}
into('lib/macosx') {
from 'libs/macosx'
}
into('lib/win') {
from 'libs/win'
}
from(project(':tools:explorer:capsule').tasks.buildExplorerJAR) {
rename 'node-explorer-(.*)', 'node-explorer.jar'
into 'explorer'
}
from(project(':node:capsule').tasks.buildCordaJAR) {
rename 'corda-(.*)', 'corda.jar'
into 'corda'
}
from(project(':samples:bank-of-corda-demo').jar) {
rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar'
into 'plugins'
}
}
}
}
/*
* Bundles the application using JavaPackager,
* using the ZIP distribution as source.
*/
task javapackage(dependsOn: 'distZip') {
doLast {
delete([pkg_source, "$buildDir/exedir"])
copy {
from(zipTree(distZip.outputs.files.singleFile))
into pkg_source
}
copy {
/*
* Copy non-text formats "as-is".
*/
from("$projectDir/package") {
exclude '**/*.spec'
exclude '**/*.sh'
exclude '**/*.wsf'
exclude '**/*.manifest'
}
into "$pkg_source/package"
}
copy {
/*
* Expand tokens for text formats.
*/
from("$projectDir/package") {
include '**/*.spec'
include '**/*.sh'
include '**/*.wsf'
include '**/*.manifest'
}
filter {
line -> line.replaceAll('@pkg_version@', pkg_version)
}
into "$pkg_source/package"
}
ant.taskdef(
resource: 'com/sun/javafx/tools/ant/antlib.xml',
classpath: "$pkg_source:$java_home/../lib/ant-javafx.jar"
)
ant.deploy(nativeBundles: packageType, outdir: "$buildDir/exedir", 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 {
fileset(dir: "$dist_source/lib", type: 'jar') {
include(name: '*.jar')
}
fileset(dir: "$dist_source/lib", type: 'native') {
include(name: "macosx/**/*.dylib")
include(name: "win/**/*.dll")
include(name: "win/**/*.exe")
include(name: "linux/**/*.so")
}
fileset(dir: dist_source, type: 'data') {
include(name: 'corda/*.jar')
include(name: 'plugins/*.jar')
include(name: 'explorer/*.jar')
}
}
platform {
property(name: 'java.util.logging.config.class', value: 'net.corda.demobench.config.LoggingConfig')
}
preferences(install: false)
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,70 @@
Summary: DemoBench
Name: demobench
Version: @pkg_version@
Release: 1
License: Unknown
Vendor: Unknown
Prefix: /opt
Provides: demobench
Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1
Autoprov: 0
Autoreq: 0
#avoid ARCH subfolder
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
#comment line below to enable effective jar compression
#it could easily get your package size from 40 to 15Mb but
#build time will substantially increase and it may require unpack200/system java to install
%define __jar_repack %{nil}
%define _javaHome %{getenv:JAVA_HOME}
%description
DemoBench
%prep
%build
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt
cp -r %{_sourcedir}/DemoBench %{buildroot}/opt
mkdir -p %{buildroot}/opt/DemoBench/runtime/bin
cp %{_javaHome}/jre/bin/java %{buildroot}/opt/DemoBench/runtime/bin
%files
/opt/DemoBench
%post
xdg-desktop-menu install --novendor /opt/DemoBench/DemoBench.desktop
if [ "false" = "true" ]; then
cp /opt/DemoBench/demobench.init /etc/init.d/demobench
if [ -x "/etc/init.d/demobench" ]; then
/sbin/chkconfig --add demobench
if [ "false" = "true" ]; then
/etc/init.d/demobench start
fi
fi
fi
%preun
xdg-desktop-menu uninstall --novendor /opt/DemoBench/DemoBench.desktop
if [ "false" = "true" ]; then
if [ -x "/etc/init.d/demobench" ]; then
if [ "true" = "true" ]; then
/etc/init.d/demobench stop
fi
/sbin/chkconfig --del demobench
rm -f /etc/init.d/demobench
fi
fi
%clean

View File

@ -0,0 +1,11 @@
if [ -z "$JAVA_HOME" ]; then
echo "**** Please set JAVA_HOME correctly."
exit 1
fi
# Switch to folder containing application.
cd ../images/image-*/DemoBench.app
INSTALL_HOME=Contents/PlugIns/Java.runtime/Contents/Home/jre/bin
mkdir -p $INSTALL_HOME
cp $JAVA_HOME/jre/bin/java $INSTALL_HOME

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<package>
<job id="postImage">
<script language="JScript">
<![CDATA[
var oShell = new ActiveXObject("wscript.shell");
var binDir = oShell.ExpandEnvironmentStrings("%JAVA_HOME%") + "\\jre\\bin\\";
var javaExe = binDir + "java.exe";
var javawExe = binDir + "javaw.exe";
var oFSO = new ActiveXObject("Scripting.FileSystemObject");
var oFolder = oFSO.getFolder(".");
var to = oFolder.path + "\\DemoBench\\runtime\\bin";
if (!oFSO.FolderExists(to)) {
oFSO.CreateFolder(to);
}
to += "\\";
oFSO.CopyFile(javaExe, to);
oFSO.CopyFile(javawExe, to);
]]>
</script>
</job>
</package>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0" processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"/>
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.VC90.CRT"
version="9.0.21022.8"
processorArchitecture="amd64"
publicKeyToken="1fc8b3b9a1e18e3b"/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel
level="asInvoker"
uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<ms_windowsSettings:dpiAware xmlns:ms_windowsSettings="http://schemas.microsoft.com/SMI/2005/WindowsSettings">false</ms_windowsSettings:dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

View File

@ -0,0 +1,34 @@
package net.corda.demobench.config;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.LogManager;
/**
* Configuration class for JUL / TornadoFX.
* Requires <code>-Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig</code>
* to be added to the JVM's command line.
*/
public class LoggingConfig {
public LoggingConfig() throws IOException {
try (InputStream input = getLoggingProperties()) {
LogManager manager = LogManager.getLogManager();
manager.readConfiguration(input);
}
}
private static InputStream getLoggingProperties() throws IOException {
ClassLoader classLoader = LoggingConfig.class.getClassLoader();
InputStream input = classLoader.getResourceAsStream("logging.properties");
if (input == null) {
Path javaHome = Paths.get(System.getProperty("java.home"));
input = Files.newInputStream(javaHome.resolve("lib").resolve("logging.properties"));
}
return input;
}
}

View File

@ -0,0 +1,42 @@
package net.corda.demobench.pty;
import com.jediterm.terminal.ProcessTtyConnector;
import com.pty4j.PtyProcess;
import com.pty4j.WinSize;
import java.nio.charset.Charset;
/**
* Copied from JediTerm pty.
* JediTerm is not available in any Maven repository.
* @author traff
*/
public class PtyProcessTtyConnector extends ProcessTtyConnector {
private final PtyProcess myProcess;
private final String name;
PtyProcessTtyConnector(String name, PtyProcess process, Charset charset) {
super(process, charset);
myProcess = process;
this.name = name;
}
@Override
protected void resizeImmediately() {
if (getPendingTermSize() != null && getPendingPixelSize() != null) {
myProcess.setWinSize(
new WinSize(getPendingTermSize().width, getPendingTermSize().height, getPendingPixelSize().width, getPendingPixelSize().height));
}
}
@Override
public boolean isConnected() {
return myProcess.isRunning();
}
@Override
public String getName() {
return name;
}
}

View File

@ -1,6 +1,11 @@
package net.corda.demobench
import javafx.scene.image.Image
import net.corda.demobench.views.DemoBenchView
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tornadofx.App
import tornadofx.addStageIcon
/**
* README!
@ -32,6 +37,22 @@ import tornadofx.App
*/
class DemoBench : App(DemoBenchView::class) {
init {
/*
* This entry point is needed by JavaPackager, as
* otherwise the packaged application cannot run.
*/
companion object {
@JvmStatic
fun main(args: Array<String>) = launch(DemoBench::class.java, *args)
}
}
init {
addStageIcon(Image("cordalogo.png"))
}
}
/*
* Trivial utility function to create SLF4J Logger.
*/
inline fun <reified T: Any> loggerFor(): Logger = LoggerFactory.getLogger(T::class.java)

View File

@ -1,13 +0,0 @@
package net.corda.demobench
import javafx.scene.Parent
import tornadofx.View
import tornadofx.importStylesheet
class DemoBenchView : View("Corda Demo Bench") {
override val root: Parent by fxml()
init {
importStylesheet("/net/corda/demobench/style.css")
}
}

View File

@ -0,0 +1,54 @@
package net.corda.demobench.model
import net.corda.demobench.loggerFor
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 {
val log = loggerFor<DBViewer>()
}
private val webServer: Server
private val pool = Executors.newCachedThreadPool()
init {
val ws = LocalWebServer()
webServer = Server(ws, "-webPort", "0")
ws.setShutdownHandler(webServer)
webServer.setShutdownHandler {
webServer.stop()
}
pool.submit {
webServer.start()
}
}
override fun close() {
log.info("Shutting down")
pool.shutdown()
webServer.shutdown()
}
fun openBrowser(h2Port: Int) {
val conn = JdbcUtils.getConnection(
org.h2.Driver::class.jvmName,
"jdbc:h2:tcp://localhost:$h2Port/node",
"sa",
""
)
val url = (webServer.service as LocalWebServer).addSession(conn)
log.info("Session: {}", url)
pool.execute {
Server.openBrowser(url)
}
}
}

View File

@ -0,0 +1,64 @@
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,18 @@
package net.corda.demobench.model
import tornadofx.Controller
class ExplorerController : Controller() {
private val jvm by inject<JVMConfig>()
private val explorerPath = jvm.applicationDir.resolve("explorer").resolve("node-explorer.jar")
init {
log.info("Explorer JAR: $explorerPath")
}
internal fun process(vararg args: String) = jvm.processFor(explorerPath, *args)
fun explorer() = Explorer(this)
}

View File

@ -0,0 +1,26 @@
package net.corda.demobench.model
import java.nio.file.Path
import java.nio.file.Paths
import tornadofx.Controller
class JVMConfig : Controller() {
val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath()
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath()
init {
log.info("Java executable: $javaPath")
}
fun commandFor(jarPath: Path, vararg args: String): Array<String> {
return arrayOf(javaPath.toString(), "-jar", jarPath.toString(), *args)
}
fun processFor(jarPath: Path, vararg args: String): ProcessBuilder {
return ProcessBuilder(commandFor(jarPath, *args).toList())
}
}

View File

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

View File

@ -0,0 +1,82 @@
package net.corda.demobench.model
import com.typesafe.config.*
import java.lang.String.join
import java.nio.file.Path
import net.corda.node.services.config.SSLConfiguration
class NodeConfig(
baseDir: Path,
legalName: String,
artemisPort: Int,
val nearestCity: String,
val webPort: Int,
val h2Port: Int,
val extraServices: List<String>,
val users: List<Map<String, Any>> = listOf(defaultUser),
var networkMap: NetworkMapConfig? = null
) : NetworkMapConfig(legalName, artemisPort) {
companion object {
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
val defaultUser: Map<String, Any> = mapOf(
"user" to "guest",
"password" to "letmein",
"permissions" to listOf(
"StartFlow.net.corda.flows.CashFlow",
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"
)
)
}
val nodeDir: Path = baseDir.resolve(key)
val explorerDir: Path = baseDir.resolve("$key-explorer")
val ssl: SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory: Path = nodeDir.resolve("certificates")
override val trustStorePassword: String = "trustpass"
override val keyStorePassword: String = "cordacadevpass"
}
var state: NodeState = NodeState.STARTING
val isCashIssuer: Boolean = extraServices.any {
it.startsWith("corda.issuer.")
}
fun isNetworkMap(): Boolean = networkMap == null
/*
* The configuration object depends upon the networkMap,
* which is mutable.
*/
fun toFileConfig(): Config = ConfigFactory.empty()
.withValue("myLegalName", valueFor(legalName))
.withValue("artemisAddress", addressValueFor(artemisPort))
.withValue("nearestCity", valueFor(nearestCity))
.withValue("extraAdvertisedServiceIds", valueFor(join(",", extraServices)))
.withFallback(optional("networkMapService", networkMap, {
c, n -> c.withValue("address", addressValueFor(n.artemisPort))
.withValue("legalName", valueFor(n.legalName))
} ))
.withValue("webAddress", addressValueFor(webPort))
.withValue("rpcUsers", valueFor(users))
.withValue("h2port", valueFor(h2Port))
.withValue("useTestClock", valueFor(true))
fun toText(): String = toFileConfig().root().render(renderOptions)
fun moveTo(baseDir: Path) = NodeConfig(
baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users, networkMap
)
}
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 {
val config = ConfigFactory.empty()
return if (obj == null) config else body(config, obj).atPath(path)
}

View File

@ -0,0 +1,159 @@
package net.corda.demobench.model
import java.io.IOException
import java.lang.management.ManagementFactory
import java.net.ServerSocket
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import net.corda.demobench.pty.R3Pty
import tornadofx.Controller
class NodeController : Controller() {
private companion object {
const val firstPort = 10000
const val minPort = 1024
const val maxPort = 65535
}
private val jvm by inject<JVMConfig>()
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 val nodes = LinkedHashMap<String, NodeConfig>()
private val port = AtomicInteger(firstPort)
private var networkMapConfig: NetworkMapConfig? = null
val activeNodes: List<NodeConfig> get() = nodes.values.filter {
(it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING)
}
init {
log.info("Base directory: $baseDir")
log.info("Corda JAR: $cordaPath")
}
fun validate(nodeData: NodeData): NodeConfig? {
val config = NodeConfig(
baseDir,
nodeData.legalName.value.trim(),
nodeData.artemisPort.value,
nodeData.nearestCity.value.trim(),
nodeData.webPort.value,
nodeData.h2Port.value,
nodeData.extraServices.value
)
if (nodes.putIfAbsent(config.key, config) != null) {
log.warning("Node with key '${config.key}' already exists.")
return null
}
// The first node becomes our network map
chooseNetworkMap(config)
return config
}
fun dispose(config: NodeConfig) {
config.state = NodeState.DEAD
if (config.networkMap == null) {
log.warning("Network map service (Node '${config.legalName}') has exited.")
}
}
val nextPort: Int get() = port.andIncrement
fun isPortAvailable(port: Int): Boolean {
if (isPortValid(port)) {
try {
ServerSocket(port).close()
return true
} catch (e: IOException) {
return false
}
} else {
return false
}
}
fun isPortValid(port: Int): Boolean = (port >= minPort) && (port <= maxPort)
fun keyExists(key: String) = nodes.keys.contains(key)
fun nameExists(name: String) = keyExists(toKey(name))
fun hasNetworkMap(): Boolean = networkMapConfig != null
fun chooseNetworkMap(config: NodeConfig) {
if (hasNetworkMap()) {
config.networkMap = networkMapConfig
} else {
networkMapConfig = config
log.info("Network map provided by: ${config.legalName}")
}
}
fun runCorda(pty: R3Pty, config: NodeConfig): Boolean {
val nodeDir = config.nodeDir.toFile()
if (nodeDir.isDirectory || nodeDir.mkdirs()) {
try {
// 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)
return false
}
} else {
return false
}
}
fun reset() {
baseDir = baseDirFor(System.currentTimeMillis())
log.info("Changed base directory: $baseDir")
// Wipe out any knowledge of previous nodes.
networkMapConfig = null
nodes.clear()
}
fun register(config: NodeConfig): Boolean {
if (nodes.putIfAbsent(config.key, config) != null) {
return false
}
if ((networkMapConfig == null) && config.isNetworkMap()) {
networkMapConfig = config
}
return true
}
fun relocate(config: NodeConfig) = config.moveTo(baseDir)
private fun baseDirFor(time: Long) = jvm.userHome.resolve("demobench").resolve(localFor(time))
private fun localFor(time: Long) = SimpleDateFormat("yyyyMMddHHmmss").format(Date(time))
}

View File

@ -0,0 +1,17 @@
package net.corda.demobench.model
import tornadofx.observable
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
class NodeData {
val legalName = SimpleStringProperty("")
val nearestCity = SimpleStringProperty("London")
val artemisPort = SimpleIntegerProperty()
val webPort = SimpleIntegerProperty()
val h2Port = SimpleIntegerProperty()
val extraServices = SimpleListProperty(mutableListOf<String>().observable())
}

View File

@ -0,0 +1,13 @@
package net.corda.demobench.model
import tornadofx.ItemViewModel
class NodeDataModel : ItemViewModel<NodeData>(NodeData()) {
val legalName = bind { item?.legalName }
val nearestCity = bind { item?.nearestCity }
val artemisPort = bind { item?.artemisPort }
val webPort = bind { item?.webPort }
val h2Port = bind { item?.h2Port }
}

View File

@ -0,0 +1,7 @@
package net.corda.demobench.model
enum class NodeState {
STARTING,
RUNNING,
DEAD
}

View File

@ -0,0 +1,34 @@
package net.corda.demobench.model
import tornadofx.Controller
import java.io.InputStreamReader
import java.net.URL
import java.util.*
class ServiceController : Controller() {
val services: List<String> = loadConf(javaClass.classLoader.getResource("services.conf"))
val notaries: List<String> = services.filter { it.startsWith("corda.notary.") }.toList()
/*
* Load our list of known extra Corda services.
*/
private fun loadConf(url: URL?): List<String> {
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)
log.info("Supports: $service")
}
}
return set.toList()
}
}
}

View File

@ -0,0 +1,57 @@
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,18 @@
package net.corda.demobench.model
import tornadofx.Controller
class WebServerController : Controller() {
private val jvm by inject<JVMConfig>()
private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
init {
log.info("Web Server JAR: $cordaPath")
}
internal fun process() = jvm.processFor(cordaPath, "--webserver")
fun webServer() = WebServer(this)
}

View File

@ -0,0 +1,131 @@
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.net.URI
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.*
import java.util.*
import java.util.function.BiPredicate
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 tornadofx.Controller
class ProfileController : Controller() {
private val jvm by inject<JVMConfig>()
private val baseDir = jvm.userHome.resolve("demobench")
private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>()
private val chooser = FileChooser()
init {
chooser.title = "DemoBench Profiles"
chooser.initialDirectory = baseDir.toFile()
chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.zip)", "*.zip", "*.ZIP"))
}
fun saveProfile(): Boolean {
val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".zip")
log.info("Save profile as: $target")
val configs = nodeController.activeNodes
// Delete the profile, if it already exists. The save
// 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")
}
}
return true
}
private fun forceExtension(target: File, ext: String): File {
return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target
}
fun openProfile(): List<NodeConfig>? {
val chosen = chooser.showOpenDialog(null) ?: return null
log.info("Selected profile: $chosen")
val configs = LinkedList<NodeConfig>()
FileSystems.newFileSystem(chosen.toPath(), null).use { fs ->
StreamSupport.stream(fs.rootDirectories.spliterator(), false)
.flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() }) }
.forEach { 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)))
log.info("Loaded: $file")
} catch (e: Exception) {
log.severe("Failed to parse '$file': ${e.message}")
throw e
}
}
}
return configs
}
private fun toNodeConfig(config: Config): NodeConfig {
val artemisPort = config.parsePort("artemisAddress")
val webPort = config.parsePort("webAddress")
val h2Port = config.getInt("h2port")
val extraServices = config.parseExtraServices("extraAdvertisedServiceIds")
val nodeConfig = NodeConfig(
baseDir, // temporary value
config.getString("myLegalName"),
artemisPort,
config.getString("nearestCity"),
webPort,
h2Port,
extraServices,
config.getObjectList("rpcUsers").map { it.unwrapped() }.toList()
)
if (config.hasPath("networkMapService")) {
val nmap = config.getConfig("networkMapService")
nodeConfig.networkMap = NetworkMapConfig(nmap.getString("legalName"), nmap.parsePort("address"))
} 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

@ -0,0 +1,62 @@
package net.corda.demobench.pty
import com.jediterm.terminal.TtyConnector
import com.jediterm.terminal.ui.*
import com.jediterm.terminal.ui.settings.SettingsProvider
import com.pty4j.PtyProcess
import net.corda.demobench.loggerFor
import java.awt.*
import java.nio.charset.StandardCharsets.UTF_8
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension, val onExit: () -> Unit) : AutoCloseable {
private companion object {
val log = loggerFor<R3Pty>()
}
private val executor = Executors.newSingleThreadExecutor()
val terminal = JediTermWidget(dimension, settings)
override fun close() {
log.info("Closing terminal '{}'", name)
executor.shutdown()
terminal.close()
}
private fun createTtyConnector(command: Array<String>, environment: Map<String, String>, workingDir: String?): TtyConnector {
val process = PtyProcess.exec(command, environment, workingDir)
try {
return PtyProcessTtyConnector(name, process, UTF_8)
} catch (e: Exception) {
process.destroyForcibly()
process.waitFor(30, TimeUnit.SECONDS)
throw e
}
}
fun run(args: Array<String>, envs: Map<String, String>, workingDir: String?) {
check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" })
val environment = HashMap<String, String>(envs)
if (!UIUtil.isWindows) {
environment["TERM"] = "xterm"
}
val connector = createTtyConnector(args, environment, workingDir)
executor.submit {
val exitValue = connector.waitFor()
log.info("Terminal has exited (value={})", exitValue)
onExit()
}
val session = terminal.createTerminalSession(connector)
session.start()
}
}

View File

@ -0,0 +1,56 @@
package net.corda.demobench.rpc
import com.google.common.net.HostAndPort
import java.util.*
import java.util.concurrent.TimeUnit.SECONDS
import net.corda.core.messaging.CordaRPCOps
import net.corda.demobench.loggerFor
import net.corda.demobench.model.NodeConfig
import net.corda.node.services.messaging.CordaRPCClient
class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable {
private companion object {
val log = loggerFor<NodeRPC>()
val oneSecond = SECONDS.toMillis(1)
}
private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.artemisPort), config.ssl)
private val timer = Timer()
init {
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)
val ops = rpcClient.proxy()
// Cancel the "setup" task now that we've created the RPC client.
this.cancel()
// Run "start-up" task, now that the RPC client is ready.
start()
// Schedule a new task that will refresh the display once per second.
timer.schedule(object: TimerTask() {
override fun run() {
invoke(ops)
}
}, 0, oneSecond)
} catch (e: Exception) {
log.warn("Node '{}' not ready yet (Error: {})", config.legalName, e.message)
}
}
}
// Wait 5 seconds for the node to start, and then poll once per second.
timer.schedule(setupTask, 5 * oneSecond, oneSecond)
}
override fun close() {
timer.cancel()
rpcClient.close()
}
}

View File

@ -0,0 +1,20 @@
package net.corda.demobench.ui
import com.sun.javafx.scene.control.behavior.TabPaneBehavior
import com.sun.javafx.scene.control.skin.TabPaneSkin
import javafx.scene.Node
import javafx.scene.control.Tab
class CloseableTab(text: String, content: Node) : Tab(text, content) {
fun requestClose() {
val b = behaviour
if ((b != null) && b.canCloseTab(this)) {
b.closeTab(this)
}
}
private val behaviour: TabPaneBehavior?
get() = (tabPane?.skin as? TabPaneSkin)?.behavior
}

View File

@ -0,0 +1,31 @@
package net.corda.demobench.ui
import javafx.scene.control.Label
import javafx.scene.layout.HBox
class PropertyLabel() : HBox() {
val nameLabel = Label()
val valueLabel = Label()
var name : String
get() = nameLabel.text
set(value) {
nameLabel.text = value
}
var value: String
get() = valueLabel.text
set(value) {
valueLabel.text = value
}
init {
nameLabel.styleClass.add("property-name")
valueLabel.styleClass.add("property-value")
children.addAll(nameLabel, valueLabel)
styleClass.add("property-label")
}
}

View File

@ -0,0 +1,126 @@
package net.corda.demobench.views
import java.util.*
import javafx.application.Platform
import javafx.scene.Parent
import javafx.scene.control.Button
import javafx.scene.control.MenuItem
import javafx.scene.control.Tab
import javafx.scene.control.TabPane
import net.corda.demobench.model.NodeConfig
import net.corda.demobench.model.NodeController
import net.corda.demobench.profile.ProfileController
import net.corda.demobench.ui.CloseableTab
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.*
class DemoBenchView : View("Corda Demo Bench") {
override val root by fxml<Parent>()
private val profileController by inject<ProfileController>()
private val nodeController by inject<NodeController>()
private val addNodeButton by fxid<Button>()
private val nodeTabPane by fxid<TabPane>()
private val menuOpen by fxid<MenuItem>()
private val menuSaveAs by fxid<MenuItem>()
init {
importStylesheet("/net/corda/demobench/style.css")
configureShutdown()
configureProfileSaveAs()
configureProfileOpen()
configureAddNode()
}
private fun configureShutdown() = primaryStage.setOnCloseRequest {
log.info("Exiting")
// Prevent any new NodeTabViews from being created.
addNodeButton.isDisable = true
closeAllTabs()
Platform.exit()
}
private fun configureProfileSaveAs() = menuSaveAs.setOnAction {
try {
if (profileController.saveProfile()) {
menuSaveAs.isDisable = true
}
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
}
}
private fun configureProfileOpen() = menuOpen.setOnAction {
try {
val profile = profileController.openProfile()
if (profile != null) {
loadProfile(profile)
}
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
}
}
private fun configureAddNode() {
addNodeButton.setOnAction {
val nodeTabView = createNodeTabView(true)
nodeTabPane.selectionModel.select(nodeTabView.nodeTab)
// Prevent us from creating new nodes until we have created the Network Map
addNodeButton.isDisable = true
}
addNodeButton.fire()
}
private fun closeAllTabs() = ArrayList<Tab>(nodeTabPane.tabs).forEach {
(it as CloseableTab).requestClose()
}
private fun createNodeTabView(showConfig: Boolean): NodeTabView {
val nodeTabView = find<NodeTabView>(mapOf("showConfig" to showConfig))
nodeTabPane.tabs.add(nodeTabView.nodeTab)
return nodeTabView
}
private fun loadProfile(nodes: List<NodeConfig>) {
closeAllTabs()
nodeController.reset()
nodes.forEach {
val nodeTabView = createNodeTabView(false)
nodeTabView.launch(nodeController.relocate(it))
}
enableAddNodes()
}
/**
* Enable the "save profile" menu item.
*/
fun enableSaveProfile() {
menuSaveAs.isDisable = false
}
/**
* Enables the button that allows us to create a new node.
*/
fun enableAddNodes() {
addNodeButton.isDisable = false
}
/**
* Ensures that DemoBench always has at least one instance NodeTabView.
* This method must NOT be called if DemoBench is shutting down.
*/
fun forceAtLeastOneTab() {
if (nodeTabPane.tabs.isEmpty()) {
addNodeButton.fire()
}
}
}

View File

@ -0,0 +1,226 @@
package net.corda.demobench.views
import java.text.DecimalFormat
import javafx.application.Platform
import javafx.scene.control.SelectionMode.MULTIPLE
import javafx.scene.layout.Pane
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.ui.CloseableTab
import tornadofx.*
class NodeTabView : Fragment() {
override val root = stackpane {}
private val main by inject<DemoBenchView>()
private val showConfig by param<Boolean>()
private companion object {
val integerFormat = DecimalFormat()
val notNumber = "[^\\d]".toRegex()
}
private val model = NodeDataModel()
private val nodeController by inject<NodeController>()
private val serviceController by inject<ServiceController>()
private val nodeTerminalView = find<NodeTerminalView>()
private val nodeConfigView = stackpane {
isVisible = showConfig
form {
fieldset("Configuration") {
field("Node Name", op = { nodeNameField() })
field("Nearest City", op = { nearestCityField() })
field("P2P Port", op = { p2pPortField() })
field("Web Port", op = { webPortField() })
field("Database Port", op = { databasePortField() })
}
fieldset("Services") {
listview(availableServices.observable()) {
selectionModel.selectionMode = MULTIPLE
model.item.extraServices.set(selectionModel.selectedItems)
}
}
button("Create Node") {
setOnAction {
if (model.validate()) {
launch()
main.enableAddNodes()
main.enableSaveProfile()
}
}
}
}
}
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()
}
root.add(nodeConfigView)
root.add(nodeTerminalView)
model.artemisPort.value = nodeController.nextPort
model.webPort.value = nodeController.nextPort
model.h2Port.value = nodeController.nextPort
}
private fun Pane.nodeNameField() = textfield(model.legalName) {
minWidth = 200.0
maxWidth = 200.0
validator {
if (it == null) {
error("Node name is required")
} else {
val name = it.trim()
if (name.isEmpty()) {
error("Node name is required")
} else if (nodeController.nameExists(name)) {
error("Node with this name already exists")
} else if (name.length > 10) {
error("Name is too long")
} else {
null
}
}
}
}
private fun Pane.nearestCityField() = textfield(model.nearestCity) {
minWidth = 200.0
maxWidth = 200.0
validator {
if (it == null) {
error("Nearest city is required")
} else if (it.trim().isEmpty()) {
error("Nearest city is required")
} else {
null
}
}
}
private fun Pane.p2pPortField() = textfield(model.artemisPort, NumberStringConverter(integerFormat)) {
minWidth = 100.0
maxWidth = 100.0
validator {
if ((it == null) || it.isEmpty()) {
error("Port number required")
} else if (it.contains(notNumber)) {
error("Invalid port number")
} else {
val port = it.toInt()
if (!nodeController.isPortAvailable(port)) {
error("Port $it is unavailable")
} else if (port == model.webPort.value) {
error("Clashes with web port")
} else if (port == model.h2Port.value) {
error("Clashes with database port")
} else {
null
}
}
}
}
private fun Pane.webPortField() = textfield(model.webPort, NumberStringConverter(integerFormat)) {
minWidth = 100.0
maxWidth = 100.0
validator {
if ((it == null) || it.isEmpty()) {
error("Port number required")
} else if (it.contains(notNumber)) {
error("Invalid port number")
} else {
val port = it.toInt()
if (!nodeController.isPortAvailable(port)) {
error("Port $it is unavailable")
} else if (port == model.artemisPort.value) {
error("Clashes with P2P port")
} else if (port == model.h2Port.value) {
error("Clashes with database port")
} else {
null
}
}
}
}
private fun Pane.databasePortField() = textfield(model.h2Port, NumberStringConverter(integerFormat)) {
minWidth = 100.0
maxWidth = 100.0
validator {
if ((it == null) || it.isEmpty()) {
error("Port number required")
} else if (it.contains(notNumber)) {
error("Invalid port number")
} else {
val port = it.toInt()
if (!nodeController.isPortAvailable(port)) {
error("Port $it is unavailable")
} else if (port == model.artemisPort.value) {
error("Clashes with P2P port")
} else if (port == model.webPort.value) {
error("Clashes with web port")
} else {
null
}
}
}
}
/**
* Launches a Corda node that was configured via the form.
*/
fun launch() {
model.commit()
val config = nodeController.validate(model.item)
if (config != null) {
nodeConfigView.isVisible = false
launchNode(config)
}
}
/**
* Launches a preconfigured Corda node, e.g. from a saved profile.
*/
fun launch(config: NodeConfig) {
nodeController.register(config)
launchNode(config)
}
private fun launchNode(config: NodeConfig) {
nodeTab.text = config.legalName
nodeTerminalView.open(config, onExit = { onTerminalExit(config) })
nodeTab.setOnSelectionChanged {
if (nodeTab.isSelected) {
// Doesn't work yet
nodeTerminalView.refreshTerminal()
}
}
}
private fun onTerminalExit(config: NodeConfig) {
Platform.runLater {
nodeTab.requestClose()
nodeController.dispose(config)
main.forceAtLeastOneTab()
}
}
}

View File

@ -0,0 +1,161 @@
package net.corda.demobench.views
import com.jediterm.terminal.TerminalColor
import com.jediterm.terminal.TextStyle
import com.jediterm.terminal.ui.settings.DefaultSettingsProvider
import java.awt.Dimension
import javafx.application.Platform
import javafx.embed.swing.SwingNode
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.layout.VBox
import javax.swing.SwingUtilities
import net.corda.demobench.model.*
import net.corda.demobench.pty.R3Pty
import net.corda.demobench.rpc.NodeRPC
import net.corda.demobench.ui.PropertyLabel
import tornadofx.Fragment
class NodeTerminalView : Fragment() {
override val root by fxml<VBox>()
private val nodeController by inject<NodeController>()
private val explorerController by inject<ExplorerController>()
private val webServerController by inject<WebServerController>()
private val nodeName by fxid<Label>()
private val p2pPort by fxid<PropertyLabel>()
private val states by fxid<PropertyLabel>()
private val transactions by fxid<PropertyLabel>()
private val balance by fxid<PropertyLabel>()
private val viewDatabaseButton by fxid<Button>()
private val launchWebButton by fxid<Button>()
private val launchExplorerButton by fxid<Button>()
private var isDestroyed: Boolean = false
private val explorer = explorerController.explorer()
private val webServer = webServerController.webServer()
private val viewer = DBViewer()
private var rpc: NodeRPC? = null
private var pty: R3Pty? = null
fun open(config: NodeConfig, onExit: () -> Unit) {
nodeName.text = config.legalName
p2pPort.value = config.artemisPort.toString()
launchWebButton.text = "Launch\nWeb Server\n(Port ${config.webPort})"
val swingTerminal = SwingNode()
swingTerminal.setOnMouseClicked {
swingTerminal.requestFocus()
}
root.children.add(swingTerminal)
root.isVisible = true
SwingUtilities.invokeLater({
val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit)
pty = r3pty
swingTerminal.content = r3pty.terminal
nodeController.runCorda(r3pty, config)
configureDatabaseButton(config)
configureExplorerButton(config)
configureWebButton(config)
/*
* Start RPC client that will update node statistics on UI.
*/
rpc = launchRPC(config)
})
}
fun enable(config: NodeConfig) {
config.state = NodeState.RUNNING
log.info("Node '${config.legalName}' is now ready.")
launchExplorerButton.isDisable = false
viewDatabaseButton.isDisable = false
launchWebButton.isDisable = false
}
/*
* We only want to run one explorer for each node.
* So disable the "launch" button when we have
* launched the explorer and only reenable it when
* the explorer has exited.
*/
fun configureExplorerButton(config: NodeConfig) {
launchExplorerButton.setOnAction {
launchExplorerButton.isDisable = true
explorer.open(config, onExit = {
launchExplorerButton.isDisable = false
})
}
}
fun configureDatabaseButton(config: NodeConfig) {
viewDatabaseButton.setOnAction {
viewer.openBrowser(config.h2Port)
}
}
/*
* We only want to run one web server for each node.
* So disable the "launch" button when we have
* launched the web server and only reenable it when
* the web server has exited.
*/
fun configureWebButton(config: NodeConfig) {
launchWebButton.setOnAction {
launchWebButton.isDisable = true
webServer.open(config, onExit = {
launchWebButton.isDisable = false
})
}
}
fun launchRPC(config: NodeConfig) = NodeRPC(config, start = { enable(config) }, invoke = { ops ->
try {
val verifiedTx = ops.verifiedTransactions()
val statesInVault = ops.vaultAndUpdates()
val cashBalances = ops.getCashBalances().entries.joinToString(
separator = ", ",
transform = { e -> e.value.toString() }
)
Platform.runLater {
states.value = statesInVault.first.size.toString()
transactions.value = verifiedTx.first.size.toString()
balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances
}
} catch (e: Exception) {
log.warning("RPC failed: " + e)
}
})
fun destroy() {
if (!isDestroyed) {
webServer.close()
explorer.close()
viewer.close()
rpc?.close()
pty?.close()
isDestroyed = true
}
}
fun refreshTerminal() {
// TODO - Force a repaint somehow? My naive attempts have not worked.
}
class TerminalSettingsProvider : DefaultSettingsProvider() {
override fun getDefaultStyle() = TextStyle(TerminalColor.WHITE, TerminalColor.BLACK)
override fun emulateX11CopyPaste() = true
}
}

View File

@ -0,0 +1,19 @@
package org.h2.server.web
import java.sql.Connection
class LocalWebServer : WebServer() {
/**
* Create a new session that will not kill the entire
* web server if/when we disconnect it.
*/
override fun addSession(conn: Connection): String {
val session = createNewSession("local")
session.setConnection(conn)
session.put("url", conn.metaData.url)
val s = session.get("sessionId") as String
return url + "/frame.jsp?jsessionid=" + s
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${user.home}/demobench/demobench.log</file>
<append>false</append>
<encoder>
<pattern>%date %-5level %c{1} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@ -0,0 +1,3 @@
# Register SLF4JBridgeHandler as handler for the j.u.l. root logger
# See http://www.slf4j.org/legacy.html#jul-to-slf4j
handlers = org.slf4j.bridge.SLF4JBridgeHandler

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<StackPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
<children>
<TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0">
<tabs>
<Tab text="Bank A">
<content>
<VBox prefHeight="953.0" prefWidth="1363.0">
<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 style="-fx-font-size: 40; -fx-text-fill: red;" text="Bank A" />
<Label style="-fx-text-fill: white;" text="P2P port: 20004" />
</children>
</VBox>
<VBox prefHeight="93.0" prefWidth="267.0">
<children>
<Label maxWidth="1.7976931348623157E308" text="States in vault: 20" />
<Label text="Known transactions: 45" />
<Label text="Balance: $1024" />
</children>
</VBox>
<Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
<Button mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="View&#10;Database" textAlignment="CENTER" />
<Button mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER" />
</children>
</HBox>
<StackPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" style="-fx-background-color: black;" VBox.vgrow="ALWAYS">
<children>
<ImageView fitWidth="1000.0" pickOnBounds="true" preserveRatio="true" StackPane.alignment="TOP_LEFT">
<image>
<Image url="@mock-term.png" />
</image>
</ImageView>
</children>
</StackPane>
</children>
</VBox>
</content>
</Tab>
<Tab text="Bank B" />
<Tab text="Bank of Corda">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0" />
</content>
</Tab>
</tabs>
</TabPane>
<Button 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

View File

@ -7,7 +7,7 @@
-fx-padding: 15px;
}
.header Label {
.property-label .label {
-fx-font-size: 14pt;
-fx-text-fill: white;
}
@ -20,4 +20,4 @@
-fx-base: #009759;
-fx-background-radius: 5px;
-fx-opacity: 80%;
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<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">
<StackPane.margin>
<Insets right="5.0" top="5.0" />
</StackPane.margin>
</Button>
</children>
</StackPane>
</VBox>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<?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,7 @@
corda.notary.validating
corda.notary.simple
corda.interest_rates
corda.issuer.USD
corda.issuer.GBP
corda.issuer.CHF
corda.cash

View File

@ -0,0 +1,59 @@
/**
* This build.gradle exists to package Node Explorer as an executable fat jar.
*/
apply plugin: 'us.kirchmeier.capsule'
description 'Node Explorer'
repositories {
mavenLocal()
mavenCentral()
maven {
url 'http://oss.sonatype.org/content/repositories/snapshots'
}
jcenter()
maven {
url 'https://dl.bintray.com/kotlin/exposed'
}
}
// Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards
// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into
// its own module so its target can be controlled individually, but for now this suffices.
sourceCompatibility = 1.6
targetCompatibility = 1.6
dependencies {
compile project(':tools:explorer')
}
task buildExplorerJAR(type: FatCapsule) {
applicationClass 'net.corda.explorer.Main'
archiveName "node-explorer-${corda_version}.jar"
applicationSource = files(project.tasks.findByName('jar'), '../build/classes/main/ExplorerCaplet.class')
classifier 'fat'
capsuleManifest {
applicationVersion = corda_version
systemProperties['visualvm.display.name'] = 'Node Explorer'
minJavaVersion = '1.8.0'
// This version is known to work and avoids earlier 8u versions that have bugs.
minUpdateVersion['1.8'] = '102'
caplets = ['ExplorerCaplet']
// JVM configuration:
// - Constrain to small heap sizes to ease development on low end devices.
// - Switch to the G1 GC which is going to be the default in Java 9 and gives low pause times/string dedup.
//
// If you change these flags, please also update Driver.kt
jvmArgs = ['-Xmx200m', '-XX:+UseG1GC']
}
manifest {
attributes('Corda-Version': corda_version)
}
}
build.dependsOn buildExplorerJAR

View File

@ -0,0 +1,49 @@
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public class ExplorerCaplet extends Capsule {
protected ExplorerCaplet(Capsule pred) {
super(pred);
}
/**
* Overriding the Caplet classpath generation via the intended interface in Capsule.
*/
@Override
@SuppressWarnings("unchecked")
protected <T> T attribute(Map.Entry<String, T> attr) {
// Equality is used here because Capsule never instantiates these attributes but instead reuses the ones
// defined as public static final fields on the Capsule class, therefore referential equality is safe.
if (ATTR_APP_CLASS_PATH == attr) {
T cp = super.attribute(attr);
List<Path> classpath = augmentClasspath((List<Path>) cp, "plugins");
return (T) augmentClasspath(classpath, "dependencies");
}
return super.attribute(attr);
}
// TODO: Make directory configurable via the capsule manifest.
// TODO: Add working directory variable to capsules string replacement variables.
private List<Path> augmentClasspath(List<Path> classpath, String dirName) {
File dir = new File(dirName);
if (!dir.exists()) {
dir.mkdir();
}
File[] files = dir.listFiles();
for (File file : files) {
if (file.isFile() && isJAR(file)) {
classpath.add(file.toPath().toAbsolutePath());
}
}
return classpath;
}
private Boolean isJAR(File file) {
return file.getName().toLowerCase().endsWith(".jar");
}
}

View File

@ -14,7 +14,6 @@ import net.corda.client.model.Models
import net.corda.client.model.observableValue
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.explorer.model.CordaViewModel
@ -28,6 +27,7 @@ import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.node.driver.PortAllocation
import net.corda.node.driver.driver
import net.corda.node.services.User
import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
@ -36,6 +36,7 @@ import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
import tornadofx.addStageIcon
import tornadofx.find
import java.nio.file.Paths
import java.util.*
/**
@ -57,9 +58,57 @@ class Main : App(MainView::class) {
}.showAndWait().get()
if (button != ButtonType.OK) it.consume()
}
stage.hide()
loginView.login()
stage.show()
val hostname = parameters.named["host"]
val port = asInteger(parameters.named["port"])
val username = parameters.named["username"]
val password = parameters.named["password"]
var isLoggedIn = false
if ((hostname != null) && (port != null) && (username != null) && (password != null)) {
try {
// Allow us optionally to override the SSL configuration too.
val sslConfig = getSSLConfig()
if (sslConfig != null) {
loginView.sslConfig = sslConfig
}
loginView.login(hostname, port, username, password)
isLoggedIn = true
} catch (e: Exception) {
ExceptionDialog(e).apply { initOwner(stage.scene.window) }.showAndWait()
}
}
if (!isLoggedIn) {
stage.hide()
loginView.login()
stage.show()
}
}
private fun asInteger(s: String?): Int? {
try {
return s?.toInt()
} catch (e: NumberFormatException) {
return null
}
}
private fun getSSLConfig(): SSLConfiguration? {
val certificatesDir = parameters.named["certificatesDir"]
val keyStorePassword = parameters.named["keyStorePassword"]
val trustStorePassword = parameters.named["trustStorePassword"]
return if ((certificatesDir != null) && (keyStorePassword != null) && (trustStorePassword != null)) {
object: SSLConfiguration {
override val certificatesDirectory = Paths.get(certificatesDir)
override val keyStorePassword: String = keyStorePassword
override val trustStorePassword: String = trustStorePassword
}
} else {
null
}
}
init {

View File

@ -38,6 +38,19 @@ class LoginView : View() {
private val keyStorePasswordProperty by objectProperty(SettingsModel::keyStorePasswordProperty)
private val trustStorePasswordProperty by objectProperty(SettingsModel::trustStorePasswordProperty)
private var sslConfigValue: SSLConfiguration = object : SSLConfiguration {
override val certificatesDirectory: Path get() = certificatesDir.get()
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
}
var sslConfig : SSLConfiguration
get() = sslConfigValue
set(value) { sslConfigValue = value }
fun login(host: String?, port: Int, username: String, password: String) {
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(host, port), configureSSL(), username, password)
}
fun login() {
val status = Dialog<LoginStatus>().apply {
dialogPane = root
@ -46,7 +59,7 @@ class LoginView : View() {
ButtonBar.ButtonData.OK_DONE -> try {
root.isDisable = true
// TODO : Run this async to avoid UI lockup.
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureSSL(), usernameTextField.text, passwordTextField.text)
login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text)
if (!rememberMe.value) {
username.value = ""
host.value = ""
@ -80,11 +93,6 @@ class LoginView : View() {
}
private fun configureSSL(): SSLConfiguration {
val sslConfig = object : SSLConfiguration {
override val certificatesDirectory: Path get() = certificatesDir.get()
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
}
// TODO : Don't use dev certificates.
return if (sslConfig.keyStoreFile.exists()) sslConfig else configureTestSSL().apply {
alert(Alert.AlertType.WARNING, "", "KeyStore not found in certificates directory.\nDEV certificates will be used by default.")