mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
Merge DemoBench into Corda. (#380)
* Add basic spec for the demobench tool. * Initial commit: Creating new tabs whenever the "Add Node" button is pressed. These tabs currently contain the bash shell only. * Refactor shutdown code, although AWT is still misbehaving. * Remove duplicate libpty native objects. * Add initial form for configuring new nodes. * Update to Corda 0.8-SNAPSHOT * Patch JediTerm to allow the application to shutdown cleanly. * Write configuration parameters into node.conf, and then run corda.jar in its own directory. * The first node now becomes the session's Network Map service used by all other nodes. Force nodes to be created one-by-one. * Trim node name and nearest city values. * Fix logging location of corda.jar * legalName field can be val. * Allow configuration of extra network services. * Launch DB viewer for node. * Small tidy-up. * Allow services to be loaded as a resources as well as a file. * Include native artifacts in distribution. * Add cash and issuer services to DemoBench. * Configure Node and DemoBench to use same version of H2 database. * Implement launching "Node Explorer" for each node. * Create a capsule for Node Explorer, and allow login via command line parameters to bypass login screen. * Simplify Kotlin objects. * Include issuer for CHF (Swiss Francs) * Fix SLF4J logging. * Display simple statistics about the node on each tab. * Add new RPC operation getCashBalances() to Node. * Ensure demobench is built after explorer:capsule. * Grant permissions to the Node's user, and install BanfOfCorda plugin for cash issuers. * Initial inclusion of Corda and BankOfCorda JARs in distribution. * Fix DemoBench distribution target. * Add SLF4J binding for Log4J 2.x * First batch of code review changes. * More changes from review. * Remove ".exe" from Java executable path, because Windows doesn't need it. * Remove superfluous lamba parameter names. * Better usage of Paths vs File API. * Simplify the configuration object. * Ensure a DemoBench installation is relocatable. * Ensure that Node Explorer can write into its working directory. * Disable Node Explorer and Database Viewer buttons until the node has launched and is responding to RPC. * Only allow the first node to run notary services. And validate port numbers more strongly. * Force all chosen port numbers to be different. * Initial javapackager task: currently builds RPMs. * Ensure JavaPackager task finds custom resources on the classpath. * Move demobench.log into the user's demobench directory. * Upgrade to Logback 1.1.10 * Make the javapackage task "more gradle" and "less ant". * Display "0" balance for a node which has no cash balances at all. * CORPRIV-665: Ensure tab closes if the node exits. * CORPRIV-665: Protect against NPE * CORPRIV-665: Protect harder against NPE * CORPRIV-665: Protect NodeTerminalView from being destroyed twice. * Initial custom resource script for Windows bundle. * Take java executable from JRE. * Allow Node Explorer to be relaunched. * CORPRIV-658: Add gradle parameter "packageType" for javapackage task. * Replace R3 logo with Corda logo. * CORPRIV-658: Add icon file for Windows installer. * CORPRIV-658: Add BAT file to create unsigned DemoBench.exe. * CORPRIV-659: Add icon file for DMG package. * Improve packaging information. * CORPRIV-660: Allow user to launch Web server for each node. * Tidy up gradle usage. * Document provenance of jediterm-terminal-2.5.jar. * Use "safe" casting operator. * CORPRIV-659: Add bin/java to minimal JRE. * CORPRIV-659: Basic shell script to package DemoBench as DMG. * Add utility function for creating SLF4J loggers, and close unused I/O streams from forked processes. * Switch from Runtime.exec() to ProcessBuilder. * CORPRIV-660: Display Web server's port number on launch button. * CORPRIV-661: Allow profiles to be loaded into DemoBench. * Upgrade to TornadoFX 1.6.2. * CORPRIV-661: Implement saving profiles. * CORPRIV-661: Refactor code for guaranteeing a .zip extension. * CORRIV-658: Add icon for Windows installer. * CORPRIV-659: Update installer script and icons for DMG. * CORPRIV-659: Tweak post-image script for DMG. * CORPRIV-658: I've wasted enough time on this - Windows rejects this BMP as invalid, and I have no idea why!? * CORPRIV-658: Add external manifest for DemoBench.exe that declares it incapable of native HiDPI support. * CORPRIV-661: Ensure that we can rewrite saved profiles correctly. * Fix terminal resizing. * CORPRIV-659: Fix DMG installer. * CORPRIV-659: Better validation for JAVA_HOME. * Downgrade JDK requirement to 8u102, for consistency with capsules. * Comment how JediTerm is not available via Maven. * CORPRIV-658: Rename packaging script. * CORPRIV-659: Renaming packaging script. * Comment file copying vs file filtering during packaging. * Fixes from code review. * CORPRIV-661: Ensure that nodes loaded from a profile have the correct network map service. * Break textfield definitions out into separate functions. * Fixes from code review. * Code review tweaks. * More code review tweaks. * Another simple code review tweak. * Replace companion object with a BiPredicate lambda. * 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. * Merged in corpriv-702 (pull request #25) CORPRIV-702: Sign the DMG with a 'Developer ID Application' certificate. * CORPRIV-702: Sign the DMG with a 'Mac Developer' certificate. * CORPRIV-702: Use "Developer ID Application" certificate instead. And now JavaPackager signs the application, which means that we only need to resign our embedded JVM. * CORPRIV-702: Update comment better to explain why JRE must be resigned. Approved-by: Mike Hearn * Exclude old version of Javassist in favour of Hibernate's version from Node. (#320) * Exclude old version of Javassist in favour of Hibernate's version. * Comment why we are excluding javassist:javassist, and add TODO for when junit-quickcheck 0.8 is released. * CORDA-265: Implement "ALL" permission for RPC users. (#306) * CORDA-265: Implement "ALL" permission for RPC users. Users with this permission in node.conf can use any flow. * CORDA-265: Ensure that we always close the RPC proxy object after each test. * CORDA-265: Refactor construction of dummy RPC client into an abstract base class. * CORDA-265: Document RPC "ALL" permission. * CORDA-266: Update DemoBench to be compatible with 0.10-SNAPSHOT. * CORDA-268: Reimplement to work on both JDK8 and JDK9 (for now). * CORDA-268: Copy java from $JAVA_HOME/bin as this also works on JDK > 8. * Code review fixes. * Use SLF4J's version of the commons-logging bindings. Only include SLF4J's Log4J back-end for actual applications, e.g. Node. (#350) * Update with SLF4J change. * CORDA-266: Update to latest node.conf format. * Upgrade to H2 1.4.194. (#389) - Timezone related fixes. - A Turkish case canonicalisation bug. - Fixes for some scary threading related bugs.
This commit is contained in:
parent
66e4f8d74b
commit
18c57cf951
@ -14,7 +14,6 @@ import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.testing.configureTestSSL
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
|
@ -16,6 +16,8 @@ include 'experimental'
|
||||
include 'experimental:sandbox'
|
||||
include 'test-utils'
|
||||
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'
|
||||
|
205
tools/demobench/build.gradle
Normal file
205
tools/demobench/build.gradle
Normal file
@ -0,0 +1,205 @@
|
||||
buildscript {
|
||||
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.controlsfx_version = '8.40.12'
|
||||
|
||||
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
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
evaluationDependsOn(':tools:explorer:capsule')
|
||||
|
||||
mainClassName = 'net.corda.demobench.DemoBench'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig', '-Dorg.jboss.logging.provider=slf4j']
|
||||
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://www.sparetimelabs.com/maven2'
|
||||
}
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile "no.tornado:tornadofx:$tornadofx_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')) {
|
||||
exclude module: 'kotlin-test'
|
||||
exclude module: 'junit'
|
||||
}
|
||||
|
||||
compile "com.h2database:h2:$h2_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.slf4j:log4j-over-slf4j:$slf4j_version"
|
||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-core:$log4j_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 "junit:junit:$junit_version"
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Corda-Version': corda_version,
|
||||
'Main-Class': mainClassName,
|
||||
'Class-Path': configurations.compile.collect { it.getName() }.join(' ')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
systemProperty 'java.util.logging.config.class', 'net.corda.demobench.config.LoggingConfig'
|
||||
systemProperty 'org.jboss.logging.provider', 'slf4j'
|
||||
}
|
||||
|
||||
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(':node:webserver:webcapsule').tasks.buildWebserverJar) {
|
||||
rename 'corda-webserver-(.*)', 'corda-webserver.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, pkg_outDir])
|
||||
|
||||
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: 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 {
|
||||
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')
|
||||
property(name: 'org.jboss.logging.provider', value: 'slf4j')
|
||||
}
|
||||
|
||||
preferences(install: false)
|
||||
}
|
||||
}
|
||||
}
|
BIN
tools/demobench/libs/jediterm-terminal-2.5.jar
Normal file
BIN
tools/demobench/libs/jediterm-terminal-2.5.jar
Normal file
Binary file not shown.
BIN
tools/demobench/libs/linux/x86/libpty.so
Executable file
BIN
tools/demobench/libs/linux/x86/libpty.so
Executable file
Binary file not shown.
BIN
tools/demobench/libs/linux/x86_64/libpty.so
Executable file
BIN
tools/demobench/libs/linux/x86_64/libpty.so
Executable file
Binary file not shown.
BIN
tools/demobench/libs/macosx/x86/libpty.dylib
Executable file
BIN
tools/demobench/libs/macosx/x86/libpty.dylib
Executable file
Binary file not shown.
BIN
tools/demobench/libs/macosx/x86_64/libpty.dylib
Executable file
BIN
tools/demobench/libs/macosx/x86_64/libpty.dylib
Executable file
Binary file not shown.
BIN
tools/demobench/libs/pty4j-0.7.2.jar
Normal file
BIN
tools/demobench/libs/pty4j-0.7.2.jar
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/libwinpty.dll
Normal file
BIN
tools/demobench/libs/win/x86/libwinpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/x86/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/winpty.dll
Normal file
BIN
tools/demobench/libs/win/x86/winpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/cyglaunch.exe
Normal file
BIN
tools/demobench/libs/win/x86_64/cyglaunch.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/x86_64/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/winpty.dll
Normal file
BIN
tools/demobench/libs/win/x86_64/winpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/xp/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/xp/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/xp/winpty.dll
Normal file
BIN
tools/demobench/libs/win/xp/winpty.dll
Normal file
Binary file not shown.
13
tools/demobench/package-demobench-dmg.sh
Executable file
13
tools/demobench/package-demobench-dmg.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=dmg javapackage $*
|
||||
echo
|
||||
echo "Wrote installer to '$(find build/javapackage/bundles -type f)'"
|
||||
echo
|
20
tools/demobench/package-demobench-exe.bat
Normal file
20
tools/demobench/package-demobench-exe.bat
Normal file
@ -0,0 +1,20 @@
|
||||
@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
|
||||
@echo
|
||||
@echo "Wrote installer to %DIRNAME%\build\javapackage\bundles\"
|
||||
@echo
|
||||
goto end
|
||||
|
||||
:NoJavaHome
|
||||
@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
|
BIN
tools/demobench/package/linux/DemoBench.png
Normal file
BIN
tools/demobench/package/linux/DemoBench.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
70
tools/demobench/package/linux/DemoBench.spec
Normal file
70
tools/demobench/package/linux/DemoBench.spec
Normal 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}/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
|
33
tools/demobench/package/macosx/DemoBench-post-image.sh
Normal file
33
tools/demobench/package/macosx/DemoBench-post-image.sh
Normal file
@ -0,0 +1,33 @@
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
echo "**** Please set JAVA_HOME correctly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function signApplication() {
|
||||
APPDIR=$1
|
||||
IDENTITY=$2
|
||||
|
||||
# Resign the embedded JRE because we have included "bin/java"
|
||||
# after javapackager had already signed the JRE installation.
|
||||
if ! (codesign --force --sign "$IDENTITY" --verbose $APPDIR/Contents/PlugIns/Java.runtime); then
|
||||
echo "**** Failed to resign the embedded JVM"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Switch to folder containing application.
|
||||
cd ../images/image-*/DemoBench.app
|
||||
|
||||
INSTALL_HOME=Contents/PlugIns/Java.runtime/Contents/Home/jre/bin
|
||||
if (mkdir -p $INSTALL_HOME); then
|
||||
cp $JAVA_HOME/bin/java $INSTALL_HOME
|
||||
fi
|
||||
|
||||
# Switch to image directory in order to sign it.
|
||||
cd ..
|
||||
|
||||
# Sign the application using a 'Developer ID Application' key on our keychain.
|
||||
if ! (signApplication DemoBench.app "Developer ID Application: "); then
|
||||
echo "**** Failed to sign the application - ABORT SIGNING"
|
||||
fi
|
||||
|
BIN
tools/demobench/package/macosx/DemoBench-volume.icns
Normal file
BIN
tools/demobench/package/macosx/DemoBench-volume.icns
Normal file
Binary file not shown.
BIN
tools/demobench/package/macosx/DemoBench.icns
Normal file
BIN
tools/demobench/package/macosx/DemoBench.icns
Normal file
Binary file not shown.
BIN
tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp
Normal file
BIN
tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
24
tools/demobench/package/windows/DemoBench-post-image.wsf
Normal file
24
tools/demobench/package/windows/DemoBench-post-image.wsf
Normal 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%") + "\\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>
|
42
tools/demobench/package/windows/DemoBench.exe.manifest
Normal file
42
tools/demobench/package/windows/DemoBench.exe.manifest
Normal 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>
|
BIN
tools/demobench/package/windows/DemoBench.ico
Normal file
BIN
tools/demobench/package/windows/DemoBench.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 KiB |
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
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!
|
||||
*
|
||||
*
|
||||
* This tool is intended to become a sales and educational tool for Corda. It is a standalone desktop app that
|
||||
* comes bundled with an appropriate JVM, and which runs nodes in a local network. It has the following features:
|
||||
*
|
||||
* - New nodes can be added at the click of a button. Clicking "Add node" creates new tab that lets you edit the
|
||||
* most important configuration properties of the node before launch, like the name and what apps will be loaded.
|
||||
*
|
||||
* - Each tab contains a terminal emulator, attached to the pty of the node. This lets you see console output and
|
||||
* (soon) interact with the command shell of the node. See the mike-crshell branch in github.
|
||||
*
|
||||
* - An Explorer instance for the node can be launched at the click of a button. Credentials are handed to the
|
||||
* Explorer so it starts out logged in already.
|
||||
*
|
||||
* - Some basic statistics are shown about each node, informed via the RPC connection.
|
||||
*
|
||||
* - Another button launches a database viewer (like the H2 web site) for the node. For instance, in an embedded
|
||||
* WebView, or the system browser.
|
||||
*
|
||||
* - It can also run a Jetty instance that can load WARs that come with the bundled CorDapps (eventually).
|
||||
*
|
||||
* The app is nicely themed using the Corda branding. It is easy enough to use for non-developers to successfully
|
||||
* demonstrate some example cordapps and why people should get excited about the platform. There is no setup
|
||||
* overhead as everything is included: just double click the icon and start going through the script. There are no
|
||||
* dependencies on external servers or network connections, so flaky conference room wifi should not be an issue.
|
||||
*/
|
||||
|
||||
class DemoBench : App(DemoBenchView::class) {
|
||||
|
||||
/*
|
||||
* 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)
|
@ -0,0 +1,71 @@
|
||||
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.rpcPort}",
|
||||
"--username=${config.users[0].user}",
|
||||
"--password=${config.users[0].password}")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.demobench.explorer
|
||||
|
||||
import net.corda.demobench.model.JVMConfig
|
||||
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)
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
interface HasPlugins {
|
||||
val pluginDir: Path
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
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 p2pPort = config.parsePort("p2pAddress")
|
||||
val rpcPort = config.parsePort("rpcAddress")
|
||||
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"),
|
||||
p2pPort,
|
||||
rpcPort,
|
||||
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.getStringList(path)
|
||||
.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)
|
||||
}
|
@ -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 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()
|
||||
|
||||
init {
|
||||
log.info("Java executable: $javaPath")
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
open class NetworkMapConfig(val legalName: String, val p2pPort: Int) {
|
||||
|
||||
val key: String = legalName.toKey()
|
||||
|
||||
}
|
||||
|
||||
private val WHITESPACE = "\\s++".toRegex()
|
||||
|
||||
fun String.stripWhitespace() = this.replace(WHITESPACE, "")
|
||||
fun String.toKey() = this.stripWhitespace().toLowerCase()
|
@ -0,0 +1,83 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import com.typesafe.config.*
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
|
||||
class NodeConfig(
|
||||
baseDir: Path,
|
||||
legalName: String,
|
||||
p2pPort: Int,
|
||||
val rpcPort: Int,
|
||||
val nearestCity: String,
|
||||
val webPort: Int,
|
||||
val h2Port: Int,
|
||||
val extraServices: List<String>,
|
||||
val users: List<User> = listOf(defaultUser),
|
||||
var networkMap: NetworkMapConfig? = null
|
||||
) : NetworkMapConfig(legalName, p2pPort), HasPlugins {
|
||||
|
||||
companion object {
|
||||
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||
val defaultUser = user("guest")
|
||||
}
|
||||
|
||||
val nodeDir: Path = baseDir.resolve(key)
|
||||
override val pluginDir: Path = nodeDir.resolve("plugins")
|
||||
val explorerDir: Path = baseDir.resolve("$key-explorer")
|
||||
|
||||
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("p2pAddress", addressValueFor(p2pPort))
|
||||
.withValue("nearestCity", valueFor(nearestCity))
|
||||
.withValue("extraAdvertisedServiceIds", valueFor(extraServices))
|
||||
.withFallback(optional("networkMapService", networkMap, {
|
||||
c, n -> c.withValue("address", addressValueFor(n.p2pPort))
|
||||
.withValue("legalName", valueFor(n.legalName))
|
||||
} ))
|
||||
.withValue("webAddress", addressValueFor(webPort))
|
||||
.withValue("rpcAddress", addressValueFor(rpcPort))
|
||||
.withValue("rpcUsers", valueFor(users.map(User::toMap).toList()))
|
||||
.withValue("h2port", valueFor(h2Port))
|
||||
.withValue("useTestClock", valueFor(true))
|
||||
|
||||
fun toText(): String = toFileConfig().root().render(renderOptions)
|
||||
|
||||
fun moveTo(baseDir: Path) = NodeConfig(
|
||||
baseDir, legalName, p2pPort, rpcPort, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
||||
|
||||
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
|
||||
|
||||
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()
|
@ -0,0 +1,189 @@
|
||||
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() {
|
||||
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: 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)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView].
|
||||
*/
|
||||
fun validate(nodeData: NodeData): NodeConfig? {
|
||||
val config = NodeConfig(
|
||||
baseDir,
|
||||
nodeData.legalName.value.trim(),
|
||||
nodeData.p2pPort.value,
|
||||
nodeData.rpcPort.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) = (port >= minPort) && (port <= maxPort)
|
||||
|
||||
fun keyExists(key: String) = nodes.keys.contains(key)
|
||||
|
||||
fun nameExists(name: String) = keyExists(name.toKey())
|
||||
|
||||
fun hasNetworkMap(): Boolean = networkMapConfig != null
|
||||
|
||||
private 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.forceDirectory()) {
|
||||
try {
|
||||
// Install any built-in plugins into the working directory.
|
||||
pluginController.populate(config)
|
||||
|
||||
// Write this node's configuration file into its working directory.
|
||||
val confFile = nodeDir.resolve("node.conf")
|
||||
confFile.writeText(config.toText())
|
||||
|
||||
// Execute the Corda node
|
||||
pty.run(command, System.getenv(), nodeDir.toString())
|
||||
log.info("Launched node: ${config.legalName}")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
log.log(Level.SEVERE, "Failed to launch Corda: ${e.message}", 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
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.p2pPort, config.rpcPort, 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))
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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 p2pPort = SimpleIntegerProperty()
|
||||
val rpcPort = SimpleIntegerProperty()
|
||||
val webPort = SimpleIntegerProperty()
|
||||
val h2Port = SimpleIntegerProperty()
|
||||
val extraServices = SimpleListProperty(mutableListOf<String>().observable())
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.ItemViewModel
|
||||
|
||||
class NodeDataModel : ItemViewModel<NodeData>(NodeData()) {
|
||||
|
||||
val legalName = bind { item?.legalName }
|
||||
val nearestCity = bind { item?.nearestCity }
|
||||
val p2pPort = bind { item?.p2pPort }
|
||||
val rpcPort = bind { item?.rpcPort }
|
||||
val webPort = bind { item?.webPort }
|
||||
val h2Port = bind { item?.h2Port }
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
enum class NodeState {
|
||||
STARTING,
|
||||
RUNNING,
|
||||
DEAD
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
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(resourceName: String = "/services.conf") : Controller() {
|
||||
|
||||
val services: List<String> = loadConf(resources.url(resourceName))
|
||||
|
||||
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 {
|
||||
try {
|
||||
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()
|
||||
} catch (e: IOException) {
|
||||
log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e)
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
data class User(val user: String, val password: String, val permissions: List<String>) {
|
||||
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("ALL"))
|
@ -0,0 +1,47 @@
|
||||
package net.corda.demobench.plugin
|
||||
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.stream.Stream
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Path.isPlugin(): Boolean = Files.isReadable(this) && this.fileName.toString().endsWith(".jar")
|
||||
fun Path.inPluginsDir(): Boolean = (this.parent != null) && this.parent.endsWith("plugins/")
|
@ -0,0 +1,140 @@
|
||||
package net.corda.demobench.profile
|
||||
|
||||
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: Path = jvm.dataHome
|
||||
private val nodeController by inject<NodeController>()
|
||||
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 (*.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, ".profile")
|
||||
log.info("Saving 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()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
private fun forceExtension(target: File, ext: String): File {
|
||||
return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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() && attr.isRegularFile }) }
|
||||
.map { file ->
|
||||
try {
|
||||
val config = installFactory.toInstallConfig(parse(file), baseDir)
|
||||
log.info("Loaded: $file")
|
||||
config
|
||||
} catch (e: Exception) {
|
||||
log.log(Level.SEVERE, "Failed to parse '$file': ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
// Java seems to "walk" through the ZIP file backwards.
|
||||
// So add new config to the front of the list, so that
|
||||
// our final list is ordered to match the file.
|
||||
}.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
private fun parse(path: Path): Config = Files.newBufferedReader(path).use {
|
||||
return ConfigFactory.parseReader(it)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
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.io.IOException
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
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.rpcPort))
|
||||
private val timer = Timer()
|
||||
|
||||
init {
|
||||
val setupTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
try {
|
||||
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.
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.demobench.ui
|
||||
|
||||
import com.sun.javafx.scene.control.behavior.TabPaneBehavior
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.Tab
|
||||
import tornadofx.*
|
||||
|
||||
/**
|
||||
* Using reflection, which works on both JDK8 and JDK9.
|
||||
* @see: <a href="https://bugs.openjdk.java.net/browse/JDK-8091261">JDK-8091261</a>
|
||||
*/
|
||||
class CloseableTab(text: String, content: Node) : Tab(text, content) {
|
||||
|
||||
fun requestClose() {
|
||||
val skin = tabPane?.skin ?: return
|
||||
val field = skin.javaClass.findFieldByName("behavior") ?: return
|
||||
field.isAccessible = true
|
||||
|
||||
val behaviour = field.get(skin) as? TabPaneBehavior ?: return
|
||||
if (behaviour.canCloseTab(this)) {
|
||||
behaviour.closeTab(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
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.InstallConfig
|
||||
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() ?: return@setOnAction
|
||||
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<InstallConfig>) {
|
||||
closeAllTabs()
|
||||
nodeController.reset()
|
||||
|
||||
nodes.forEach {
|
||||
val nodeTabView = createNodeTabView(false)
|
||||
nodeTabView.launch(nodeController.install(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()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,297 @@
|
||||
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.*
|
||||
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(true)
|
||||
|
||||
private companion object : Component() {
|
||||
const val textWidth = 200.0
|
||||
const val numberWidth = 100.0
|
||||
const val maxNameLength = 10
|
||||
|
||||
val integerFormat = DecimalFormat()
|
||||
val notNumber = "[^\\d]".toRegex()
|
||||
|
||||
val jvm by inject<JVMConfig>()
|
||||
|
||||
init {
|
||||
integerFormat.isGroupingUsed = false
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
isVisible = showConfig
|
||||
|
||||
form {
|
||||
fieldset("Configuration") {
|
||||
isFillWidth = false
|
||||
|
||||
field("Node Name", op = { nodeNameField() })
|
||||
field("Nearest City", op = { nearestCityField() })
|
||||
field("P2P Port", op = { p2pPortField() })
|
||||
field("RPC port", op = { rpcPortField() })
|
||||
field("Web Port", op = { webPortField() })
|
||||
field("Database Port", op = { databasePortField() })
|
||||
}
|
||||
|
||||
hbox {
|
||||
styleClass.addAll("node-panel")
|
||||
|
||||
fieldset("Services") {
|
||||
styleClass.addAll("services-panel")
|
||||
|
||||
listview(availableServices.observable()) {
|
||||
selectionModel.selectionMode = MULTIPLE
|
||||
model.item.extraServices.set(selectionModel.selectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
fieldset("Cordapps") {
|
||||
styleClass.addAll("cordapps-panel")
|
||||
|
||||
listview(cordapps) {
|
||||
setOnKeyPressed { key ->
|
||||
if ((key.code == KeyCode.DELETE) && !selectionModel.isEmpty) {
|
||||
cordapps.remove(selectionModel.selectedItem)
|
||||
}
|
||||
key.consume()
|
||||
}
|
||||
}
|
||||
button("Add Cordapp") {
|
||||
setOnAction {
|
||||
val app = (chooser.showOpenDialog(null) ?: return@setOnAction).toPath()
|
||||
if (!cordapps.contains(app)) {
|
||||
cordapps.add(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button("Create Node") {
|
||||
setOnAction {
|
||||
if (model.validate()) {
|
||||
launch()
|
||||
main.enableAddNodes()
|
||||
main.enableSaveProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nodeTab = CloseableTab("New Node", root)
|
||||
|
||||
init {
|
||||
// Ensure that we destroy the terminal along with the tab.
|
||||
nodeTab.setOnCloseRequest {
|
||||
nodeTerminalView.destroy()
|
||||
}
|
||||
|
||||
root.add(nodeConfigView)
|
||||
root.add(nodeTerminalView)
|
||||
|
||||
model.p2pPort.value = nodeController.nextPort
|
||||
model.rpcPort.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 = textWidth
|
||||
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 > maxNameLength) {
|
||||
error("Name is too long")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.nearestCityField() = textfield(model.nearestCity) {
|
||||
minWidth = textWidth
|
||||
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.p2pPort, NumberStringConverter(integerFormat)) {
|
||||
minWidth = numberWidth
|
||||
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.rpcPort.value) {
|
||||
error("Clashes with RPC port")
|
||||
} 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.rpcPortField() = textfield(model.rpcPort, NumberStringConverter(integerFormat)) {
|
||||
minWidth = 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.p2pPort.value) {
|
||||
error("Clashes with P2P port")
|
||||
} 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 = numberWidth
|
||||
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.p2pPort.value) {
|
||||
error("Clashes with P2P port")
|
||||
} else if (port == model.rpcPort.value) {
|
||||
error("Clashes with RPC port")
|
||||
} else if (port == model.h2Port.value) {
|
||||
error("Clashes with database port")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.databasePortField() = textfield(model.h2Port, NumberStringConverter(integerFormat)) {
|
||||
minWidth = numberWidth
|
||||
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.p2pPort.value) {
|
||||
error("Clashes with P2P port")
|
||||
} else if (port == model.rpcPort.value) {
|
||||
error("Clashes with RPC 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
|
||||
config.install(cordapps)
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
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 java.util.logging.Level
|
||||
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.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() {
|
||||
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.p2pPort.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.log(Level.WARNING, "RPC failed: ${e.message}", 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
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
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
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@Throws(SQLException::class)
|
||||
fun openBrowser(h2Port: Int) {
|
||||
val conn = JdbcUtils.getConnection(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.demobench.web
|
||||
|
||||
import net.corda.demobench.model.JVMConfig
|
||||
import tornadofx.Controller
|
||||
|
||||
class WebServerController : Controller() {
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
private val webserverPath = jvm.applicationDir.resolve("corda").resolve("corda-webserver.jar")
|
||||
|
||||
init {
|
||||
log.info("Web Server JAR: $webserverPath")
|
||||
}
|
||||
|
||||
internal fun process() = jvm.processFor(webserverPath)
|
||||
|
||||
fun webServer() = WebServer(this)
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.h2.server.web
|
||||
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
|
||||
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)
|
||||
session.put("url", conn.metaData.url)
|
||||
val s = session.get("sessionId") as String
|
||||
return url + "/frame.jsp?jsessionid=" + s
|
||||
}
|
||||
|
||||
}
|
BIN
tools/demobench/src/main/resources/cordalogo.png
Normal file
BIN
tools/demobench/src/main/resources/cordalogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
47
tools/demobench/src/main/resources/log4j2.xml
Normal file
47
tools/demobench/src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Properties>
|
||||
<Property name="log-path">${sys:user.home}/demobench</Property>
|
||||
<Property name="log-name">demobench</Property>
|
||||
<Property name="archive">${sys:log-path}/archive</Property>
|
||||
<Property name="consoleLogLevel">error</Property>
|
||||
<Property name="defaultLogLevel">info</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${sys:log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="%d{ISO8601}{GMT+0} [%-5level] %c{1} - %msg%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="10">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="${sys:defaultLogLevel}">
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
3
tools/demobench/src/main/resources/logging.properties
Normal file
3
tools/demobench/src/main/resources/logging.properties
Normal 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
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* https://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html
|
||||
* https://r3-cev.atlassian.net/wiki/display/RH/Color+Palettes
|
||||
*/
|
||||
|
||||
.header {
|
||||
-fx-background-color: #505050;
|
||||
-fx-padding: 15px;
|
||||
}
|
||||
|
||||
.property-label .label {
|
||||
-fx-font-size: 14pt;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.add-node-button {
|
||||
-fx-base: red;
|
||||
}
|
||||
|
||||
.big-button {
|
||||
-fx-base: #009759;
|
||||
-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;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?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 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" />
|
||||
</StackPane.margin>
|
||||
</Button>
|
||||
</StackPane>
|
||||
</VBox>
|
@ -0,0 +1,26 @@
|
||||
<?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">
|
||||
<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>
|
7
tools/demobench/src/main/resources/services.conf
Normal file
7
tools/demobench/src/main/resources/services.conf
Normal 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
|
@ -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,193 @@
|
||||
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 P2P port`() {
|
||||
val config = createConfig(p2pPort = 10001)
|
||||
assertEquals(10001, config.p2pPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rpc port`() {
|
||||
val config = createConfig(rpcPort = 40002)
|
||||
assertEquals(40002, config.rpcPort)
|
||||
}
|
||||
|
||||
@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 config text`() {
|
||||
val config = createConfig(
|
||||
legalName = "My Name",
|
||||
nearestCity = "Stockholm",
|
||||
p2pPort = 10001,
|
||||
rpcPort = 40002,
|
||||
webPort = 20001,
|
||||
h2Port = 30001,
|
||||
services = listOf("my.service"),
|
||||
users = listOf(user("jenny"))
|
||||
)
|
||||
assertEquals("{"
|
||||
+ "\"extraAdvertisedServiceIds\":[\"my.service\"],"
|
||||
+ "\"h2port\":30001,"
|
||||
+ "\"myLegalName\":\"MyName\","
|
||||
+ "\"nearestCity\":\"Stockholm\","
|
||||
+ "\"p2pAddress\":\"localhost:10001\","
|
||||
+ "\"rpcAddress\":\"localhost:40002\","
|
||||
+ "\"rpcUsers\":["
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"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",
|
||||
p2pPort = 10001,
|
||||
rpcPort = 40002,
|
||||
webPort = 20001,
|
||||
h2Port = 30001,
|
||||
services = listOf("my.service"),
|
||||
users = listOf(user("jenny"))
|
||||
)
|
||||
config.networkMap = NetworkMapConfig("Notary", 12345)
|
||||
|
||||
assertEquals("{"
|
||||
+ "\"extraAdvertisedServiceIds\":[\"my.service\"],"
|
||||
+ "\"h2port\":30001,"
|
||||
+ "\"myLegalName\":\"MyName\","
|
||||
+ "\"nearestCity\":\"Stockholm\","
|
||||
+ "\"networkMapService\":{\"address\":\"localhost:12345\",\"legalName\":\"Notary\"},"
|
||||
+ "\"p2pAddress\":\"localhost:10001\","
|
||||
+ "\"rpcAddress\":\"localhost:40002\","
|
||||
+ "\"rpcUsers\":["
|
||||
+ "{\"password\":\"letmein\",\"permissions\":[\"ALL\"],\"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)
|
||||
}
|
||||
|
||||
private fun createConfig(
|
||||
legalName: String = "Unknown",
|
||||
nearestCity: String = "Nowhere",
|
||||
p2pPort: Int = -1,
|
||||
rpcPort: 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,
|
||||
p2pPort = p2pPort,
|
||||
rpcPort = rpcPort,
|
||||
webPort = webPort,
|
||||
h2Port = h2Port,
|
||||
extraServices = services,
|
||||
users = users
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
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.p2pPort.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 P2P port is max`() {
|
||||
val portNumber = NodeController.firstPort + 1234
|
||||
val config = createConfig(p2pPort = portNumber)
|
||||
assertEquals(NodeController.firstPort, controller.nextPort)
|
||||
controller.register(config)
|
||||
assertEquals(portNumber + 1, controller.nextPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rpc port is max`() {
|
||||
val portNumber = NodeController.firstPort + 7777
|
||||
val config = createConfig(rpcPort = portNumber)
|
||||
assertEquals(NodeController.firstPort, controller.nextPort)
|
||||
controller.register(config)
|
||||
assertEquals(portNumber + 1, controller.nextPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test web port is max`() {
|
||||
val portNumber = NodeController.firstPort + 2356
|
||||
val config = createConfig(webPort = portNumber)
|
||||
assertEquals(NodeController.firstPort, controller.nextPort)
|
||||
controller.register(config)
|
||||
assertEquals(portNumber + 1, controller.nextPort)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test H2 port is max`() {
|
||||
val portNumber = NodeController.firstPort + 3478
|
||||
val config = createConfig(h2Port = portNumber)
|
||||
assertEquals(NodeController.firstPort, controller.nextPort)
|
||||
controller.register(config)
|
||||
assertEquals(portNumber + 1, 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",
|
||||
p2pPort: Int = -1,
|
||||
rpcPort: 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,
|
||||
p2pPort = p2pPort,
|
||||
rpcPort = rpcPort,
|
||||
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,46 @@
|
||||
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 `default user`() {
|
||||
val user = user("guest")
|
||||
assertEquals("guest", user.user)
|
||||
assertEquals("letmein", user.password)
|
||||
assertEquals(listOf("ALL"), user.permissions)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
corda.example
|
||||
corda.example
|
||||
corda.example
|
15
tools/demobench/src/test/resources/log4j2-test.xml
Normal file
15
tools/demobench/src/test/resources/log4j2-test.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%date %highlight{%level %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</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
|
59
tools/explorer/capsule/build.gradle
Normal file
59
tools/explorer/capsule/build.gradle
Normal 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
|
49
tools/explorer/src/main/java/ExplorerCaplet.java
Normal file
49
tools/explorer/src/main/java/ExplorerCaplet.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
@ -55,9 +55,35 @@ 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -7,7 +7,7 @@ import net.corda.client.model.NodeMonitorModel
|
||||
import net.corda.client.model.objectProperty
|
||||
import net.corda.explorer.model.SettingsModel
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import tornadofx.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class LoginView : View() {
|
||||
@ -27,6 +27,10 @@ class LoginView : View() {
|
||||
private val port by objectProperty(SettingsModel::portProperty)
|
||||
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
|
||||
|
||||
fun login(host: String?, port: Int, username: String, password: String) {
|
||||
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(host, port), username, password)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val status = Dialog<LoginStatus>().apply {
|
||||
dialogPane = root
|
||||
@ -35,7 +39,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), usernameTextField.text, passwordTextField.text)
|
||||
login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text)
|
||||
if (!rememberMe.value) {
|
||||
username.value = ""
|
||||
host.value = ""
|
||||
@ -81,4 +85,4 @@ class LoginView : View() {
|
||||
private enum class LoginStatus {
|
||||
loggedIn, exited, exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user