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:
Chris Rankin 2017-03-21 14:24:13 +00:00 committed by GitHub
parent 66e4f8d74b
commit 18c57cf951
80 changed files with 3126 additions and 7 deletions

View File

@ -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

View File

@ -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'

View 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)
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View 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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

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

View File

@ -0,0 +1,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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

View File

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

View File

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

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

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

View File

@ -0,0 +1,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)
}

View File

@ -0,0 +1,26 @@
package net.corda.demobench.model
import java.nio.file.Path
import java.nio.file.Paths
import tornadofx.Controller
class JVMConfig : Controller() {
val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath()
val 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))
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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))
}

View File

@ -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())
}

View File

@ -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 }
}

View File

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

View File

@ -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()
}
}
}
}

View File

@ -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"))

View File

@ -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/")

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

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

View File

@ -0,0 +1,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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

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

View File

@ -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)
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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>

View File

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

View File

@ -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;
}

View File

@ -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>

View File

@ -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&#10;Database" textAlignment="CENTER" />
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Web Server" textAlignment="CENTER" />
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch&#10;Explorer" textAlignment="CENTER" />
</HBox>
</VBox>

View File

@ -0,0 +1,7 @@
corda.notary.validating
corda.notary.simple
corda.interest_rates
corda.issuer.USD
corda.issuer.GBP
corda.issuer.CHF
corda.cash

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
)
}

View File

@ -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
)
}

View File

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

View File

@ -0,0 +1,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)
}
}

View File

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

View 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>

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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
}
}
}