mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Merged in demobench (pull request #16)
Demobench Approved-by: Clinton Alexander Approved-by: Shams Asari
This commit is contained in:
commit
438fc97881
@ -29,6 +29,7 @@ buildscript {
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.h2_version = '1.4.193'
|
||||
ext.dokka_version = '0.9.13'
|
||||
|
||||
repositories {
|
||||
|
@ -11,3 +11,11 @@ dataSourceProperties {
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
h2port = 0
|
||||
|
||||
jiraConfig{
|
||||
address = "https://doorman-jira-host/"
|
||||
projectCode = "TD"
|
||||
username = "username"
|
||||
password = "password"
|
||||
doneTransitionCode = 41
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ dependencies {
|
||||
testCompile project(':core')
|
||||
|
||||
// For H2 database support in persistence
|
||||
compile "com.h2database:h2:1.4.193"
|
||||
compile "com.h2database:h2:$h2_version"
|
||||
|
||||
// Exposed: Kotlin SQL library - under evaluation
|
||||
// TODO: Upgrade to Exposed 0.7 (has API changes)
|
||||
|
10
package-demobench-dmg.sh
Executable file
10
package-demobench-dmg.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
DIRNAME=$(dirname $0)
|
||||
|
||||
if [ -z "$JAVA_HOME" -o ! -x $JAVA_HOME/bin/java ]; then
|
||||
echo "Please set JAVA_HOME correctly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec $DIRNAME/gradlew -PpackageType=dmg javapackage $*
|
17
package-demobench-exe.bat
Normal file
17
package-demobench-exe.bat
Normal file
@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
|
||||
@rem Creates an EXE installer for DemoBench.
|
||||
@rem Assumes that Inno Setup 5+ has already been installed (http://www.jrsoftware.org/isinfo.php)
|
||||
|
||||
if not defined JAVA_HOME goto NoJavaHome
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
|
||||
call %DIRNAME%\gradlew -PpackageType=exe javapackage
|
||||
goto end
|
||||
|
||||
:NoJavaHome
|
||||
echo "Please set JAVA_HOME correctly"
|
||||
|
||||
:end
|
@ -11,8 +11,9 @@ include 'doorman'
|
||||
include 'experimental'
|
||||
include 'experimental:sandbox'
|
||||
include 'test-utils'
|
||||
include 'tools:demobench'
|
||||
include 'tools:explorer'
|
||||
include 'tools:explorer:capsule'
|
||||
include 'tools:demobench'
|
||||
include 'tools:loadtest'
|
||||
include 'docs/source/example-code' // Note that we are deliberately choosing to use '/' here. With ':' gradle would treat the directories as actual projects.
|
||||
include 'samples:attachment-demo'
|
||||
|
@ -1,10 +1,20 @@
|
||||
group 'net.corda'
|
||||
version '0.7-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.6'
|
||||
ext.tornadofx_version = '1.6.2'
|
||||
ext.jna_version = '4.1.0'
|
||||
ext.purejavacomm_version = '0.0.17'
|
||||
ext.guava_version = '14.0.1'
|
||||
ext.slf4j_version = '1.7.22'
|
||||
ext.logback_version = '1.1.10'
|
||||
ext.controlsfx_version = '8.40.12'
|
||||
|
||||
ext.java_home = System.properties.'java.home'
|
||||
ext.pkg_source = "$buildDir/packagesrc"
|
||||
ext.dist_source = "$pkg_source/demobench-$version"
|
||||
ext.pkg_version = "$version".indexOf('-') >= 0 ? "$version".substring(0, "$version".indexOf('-')) : version
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
@ -14,17 +24,185 @@ buildscript {
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
evaluationDependsOn(':tools:explorer:capsule')
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
mainClassName = 'net.corda.demobench.DemoBench'
|
||||
applicationDefaultJvmArgs = ['-Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig']
|
||||
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://www.sparetimelabs.com/maven2'
|
||||
}
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile 'no.tornado:tornadofx:1.5.7'
|
||||
compile "no.tornado:tornadofx:$tornadofx_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
|
||||
compile "org.controlsfx:controlsfx:$controlsfx_version"
|
||||
|
||||
// ONLY USING THE RPC CLIENT!?
|
||||
compile project(':node')
|
||||
|
||||
compile "com.h2database:h2:$h2_version"
|
||||
compile "net.java.dev.jna:jna:$jna_version"
|
||||
compile "net.java.dev.jna:jna-platform:$jna_version"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
compile "com.sparetimelabs:purejavacomm:$purejavacomm_version"
|
||||
compile "org.apache.logging.log4j:log4j-to-slf4j:$log4j_version"
|
||||
compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
|
||||
compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
|
||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||
compile "ch.qos.logback:logback-classic:$logback_version"
|
||||
compile "com.typesafe:config:$typesafe_config_version"
|
||||
|
||||
// These libraries don't exist in any Maven repository I can find.
|
||||
// See: https://github.com/JetBrains/jediterm
|
||||
//
|
||||
// The terminal JAR here has also been tweaked:
|
||||
// See: https://github.com/JetBrains/jediterm/issues/144
|
||||
compile ':jediterm-terminal-2.5'
|
||||
compile ':pty4j-0.7.2'
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: junit_version
|
||||
}
|
||||
|
||||
// We don't want the Node to drag these transitive dependencies in either!
|
||||
configurations.all {
|
||||
exclude module: 'commons-logging'
|
||||
exclude module: 'log4j-slf4j-impl'
|
||||
exclude module: 'log4j-core'
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Corda-Version': corda_version,
|
||||
'Main-Class': mainClassName,
|
||||
'Class-Path': configurations.compile.collect { it.getName() }.join(' ')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
distributions {
|
||||
main() {
|
||||
contents {
|
||||
into('lib/linux') {
|
||||
from 'libs/linux'
|
||||
}
|
||||
into('lib/macosx') {
|
||||
from 'libs/macosx'
|
||||
}
|
||||
into('lib/win') {
|
||||
from 'libs/win'
|
||||
}
|
||||
from(project(':tools:explorer:capsule').tasks.buildExplorerJAR) {
|
||||
rename 'node-explorer-(.*)', 'node-explorer.jar'
|
||||
into 'explorer'
|
||||
}
|
||||
from(project(':node:capsule').tasks.buildCordaJAR) {
|
||||
rename 'corda-(.*)', 'corda.jar'
|
||||
into 'corda'
|
||||
}
|
||||
from(project(':samples:bank-of-corda-demo').jar) {
|
||||
rename 'bank-of-corda-demo-(.*)', 'bank-of-corda.jar'
|
||||
into 'plugins'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Bundles the application using JavaPackager,
|
||||
* using the ZIP distribution as source.
|
||||
*/
|
||||
task javapackage(dependsOn: 'distZip') {
|
||||
|
||||
doLast {
|
||||
delete([pkg_source, "$buildDir/exedir"])
|
||||
|
||||
copy {
|
||||
from(zipTree(distZip.outputs.files.singleFile))
|
||||
into pkg_source
|
||||
}
|
||||
|
||||
copy {
|
||||
/*
|
||||
* Copy non-text formats "as-is".
|
||||
*/
|
||||
from("$projectDir/package") {
|
||||
exclude '**/*.spec'
|
||||
exclude '**/*.sh'
|
||||
exclude '**/*.wsf'
|
||||
exclude '**/*.manifest'
|
||||
}
|
||||
into "$pkg_source/package"
|
||||
}
|
||||
|
||||
copy {
|
||||
/*
|
||||
* Expand tokens for text formats.
|
||||
*/
|
||||
from("$projectDir/package") {
|
||||
include '**/*.spec'
|
||||
include '**/*.sh'
|
||||
include '**/*.wsf'
|
||||
include '**/*.manifest'
|
||||
}
|
||||
filter {
|
||||
line -> line.replaceAll('@pkg_version@', pkg_version)
|
||||
}
|
||||
into "$pkg_source/package"
|
||||
}
|
||||
|
||||
ant.taskdef(
|
||||
resource: 'com/sun/javafx/tools/ant/antlib.xml',
|
||||
classpath: "$pkg_source:$java_home/../lib/ant-javafx.jar"
|
||||
)
|
||||
|
||||
ant.deploy(nativeBundles: packageType, outdir: "$buildDir/exedir", outfile: 'DemoBench', verbose: 'true') {
|
||||
application(name: 'DemoBench', version: pkg_version, mainClass: mainClassName)
|
||||
info(title: 'DemoBench', vendor: 'R3', description: 'A sales and educational tool for demonstrating Corda.')
|
||||
resources {
|
||||
fileset(dir: "$dist_source/lib", type: 'jar') {
|
||||
include(name: '*.jar')
|
||||
}
|
||||
|
||||
fileset(dir: "$dist_source/lib", type: 'native') {
|
||||
include(name: "macosx/**/*.dylib")
|
||||
include(name: "win/**/*.dll")
|
||||
include(name: "win/**/*.exe")
|
||||
include(name: "linux/**/*.so")
|
||||
}
|
||||
|
||||
fileset(dir: dist_source, type: 'data') {
|
||||
include(name: 'corda/*.jar')
|
||||
include(name: 'plugins/*.jar')
|
||||
include(name: 'explorer/*.jar')
|
||||
}
|
||||
}
|
||||
|
||||
platform {
|
||||
property(name: 'java.util.logging.config.class', value: 'net.corda.demobench.config.LoggingConfig')
|
||||
}
|
||||
|
||||
preferences(install: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
tools/demobench/libs/jediterm-terminal-2.5.jar
Normal file
BIN
tools/demobench/libs/jediterm-terminal-2.5.jar
Normal file
Binary file not shown.
BIN
tools/demobench/libs/linux/x86/libpty.so
Executable file
BIN
tools/demobench/libs/linux/x86/libpty.so
Executable file
Binary file not shown.
BIN
tools/demobench/libs/linux/x86_64/libpty.so
Executable file
BIN
tools/demobench/libs/linux/x86_64/libpty.so
Executable file
Binary file not shown.
BIN
tools/demobench/libs/macosx/x86/libpty.dylib
Executable file
BIN
tools/demobench/libs/macosx/x86/libpty.dylib
Executable file
Binary file not shown.
BIN
tools/demobench/libs/macosx/x86_64/libpty.dylib
Executable file
BIN
tools/demobench/libs/macosx/x86_64/libpty.dylib
Executable file
Binary file not shown.
BIN
tools/demobench/libs/pty4j-0.7.2.jar
Normal file
BIN
tools/demobench/libs/pty4j-0.7.2.jar
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/libwinpty.dll
Normal file
BIN
tools/demobench/libs/win/x86/libwinpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/x86/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86/winpty.dll
Normal file
BIN
tools/demobench/libs/win/x86/winpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/cyglaunch.exe
Normal file
BIN
tools/demobench/libs/win/x86_64/cyglaunch.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/x86_64/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/x86_64/winpty.dll
Normal file
BIN
tools/demobench/libs/win/x86_64/winpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/xp/winpty-agent.exe
Normal file
BIN
tools/demobench/libs/win/xp/winpty-agent.exe
Normal file
Binary file not shown.
BIN
tools/demobench/libs/win/xp/winpty.dll
Normal file
BIN
tools/demobench/libs/win/xp/winpty.dll
Normal file
Binary file not shown.
BIN
tools/demobench/package/linux/DemoBench.png
Normal file
BIN
tools/demobench/package/linux/DemoBench.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
70
tools/demobench/package/linux/DemoBench.spec
Normal file
70
tools/demobench/package/linux/DemoBench.spec
Normal file
@ -0,0 +1,70 @@
|
||||
Summary: DemoBench
|
||||
Name: demobench
|
||||
Version: @pkg_version@
|
||||
Release: 1
|
||||
License: Unknown
|
||||
Vendor: Unknown
|
||||
Prefix: /opt
|
||||
Provides: demobench
|
||||
Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
|
||||
#avoid ARCH subfolder
|
||||
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
%define _javaHome %{getenv:JAVA_HOME}
|
||||
|
||||
%description
|
||||
DemoBench
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}/opt
|
||||
cp -r %{_sourcedir}/DemoBench %{buildroot}/opt
|
||||
mkdir -p %{buildroot}/opt/DemoBench/runtime/bin
|
||||
cp %{_javaHome}/jre/bin/java %{buildroot}/opt/DemoBench/runtime/bin
|
||||
|
||||
%files
|
||||
|
||||
/opt/DemoBench
|
||||
|
||||
%post
|
||||
|
||||
|
||||
xdg-desktop-menu install --novendor /opt/DemoBench/DemoBench.desktop
|
||||
|
||||
if [ "false" = "true" ]; then
|
||||
cp /opt/DemoBench/demobench.init /etc/init.d/demobench
|
||||
if [ -x "/etc/init.d/demobench" ]; then
|
||||
/sbin/chkconfig --add demobench
|
||||
if [ "false" = "true" ]; then
|
||||
/etc/init.d/demobench start
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
%preun
|
||||
|
||||
xdg-desktop-menu uninstall --novendor /opt/DemoBench/DemoBench.desktop
|
||||
|
||||
if [ "false" = "true" ]; then
|
||||
if [ -x "/etc/init.d/demobench" ]; then
|
||||
if [ "true" = "true" ]; then
|
||||
/etc/init.d/demobench stop
|
||||
fi
|
||||
/sbin/chkconfig --del demobench
|
||||
rm -f /etc/init.d/demobench
|
||||
fi
|
||||
fi
|
||||
|
||||
%clean
|
11
tools/demobench/package/macosx/DemoBench-post-image.sh
Normal file
11
tools/demobench/package/macosx/DemoBench-post-image.sh
Normal file
@ -0,0 +1,11 @@
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
echo "**** Please set JAVA_HOME correctly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Switch to folder containing application.
|
||||
cd ../images/image-*/DemoBench.app
|
||||
|
||||
INSTALL_HOME=Contents/PlugIns/Java.runtime/Contents/Home/jre/bin
|
||||
mkdir -p $INSTALL_HOME
|
||||
cp $JAVA_HOME/jre/bin/java $INSTALL_HOME
|
BIN
tools/demobench/package/macosx/DemoBench-volume.icns
Normal file
BIN
tools/demobench/package/macosx/DemoBench-volume.icns
Normal file
Binary file not shown.
BIN
tools/demobench/package/macosx/DemoBench.icns
Normal file
BIN
tools/demobench/package/macosx/DemoBench.icns
Normal file
Binary file not shown.
BIN
tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp
Normal file
BIN
tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
24
tools/demobench/package/windows/DemoBench-post-image.wsf
Normal file
24
tools/demobench/package/windows/DemoBench-post-image.wsf
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" ?>
|
||||
<package>
|
||||
<job id="postImage">
|
||||
<script language="JScript">
|
||||
<![CDATA[
|
||||
var oShell = new ActiveXObject("wscript.shell");
|
||||
var binDir = oShell.ExpandEnvironmentStrings("%JAVA_HOME%") + "\\jre\\bin\\";
|
||||
var javaExe = binDir + "java.exe";
|
||||
var javawExe = binDir + "javaw.exe";
|
||||
|
||||
var oFSO = new ActiveXObject("Scripting.FileSystemObject");
|
||||
var oFolder = oFSO.getFolder(".");
|
||||
var to = oFolder.path + "\\DemoBench\\runtime\\bin";
|
||||
if (!oFSO.FolderExists(to)) {
|
||||
oFSO.CreateFolder(to);
|
||||
}
|
||||
to += "\\";
|
||||
|
||||
oFSO.CopyFile(javaExe, to);
|
||||
oFSO.CopyFile(javawExe, to);
|
||||
]]>
|
||||
</script>
|
||||
</job>
|
||||
</package>
|
42
tools/demobench/package/windows/DemoBench.exe.manifest
Normal file
42
tools/demobench/package/windows/DemoBench.exe.manifest
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0" processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.VC90.CRT"
|
||||
version="9.0.21022.8"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="1fc8b3b9a1e18e3b"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel
|
||||
level="asInvoker"
|
||||
uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<ms_windowsSettings:dpiAware xmlns:ms_windowsSettings="http://schemas.microsoft.com/SMI/2005/WindowsSettings">false</ms_windowsSettings:dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
|
||||
</assembly>
|
BIN
tools/demobench/package/windows/DemoBench.ico
Normal file
BIN
tools/demobench/package/windows/DemoBench.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 KiB |
@ -0,0 +1,34 @@
|
||||
package net.corda.demobench.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.logging.LogManager;
|
||||
|
||||
/**
|
||||
* Configuration class for JUL / TornadoFX.
|
||||
* Requires <code>-Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig</code>
|
||||
* to be added to the JVM's command line.
|
||||
*/
|
||||
public class LoggingConfig {
|
||||
|
||||
public LoggingConfig() throws IOException {
|
||||
try (InputStream input = getLoggingProperties()) {
|
||||
LogManager manager = LogManager.getLogManager();
|
||||
manager.readConfiguration(input);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream getLoggingProperties() throws IOException {
|
||||
ClassLoader classLoader = LoggingConfig.class.getClassLoader();
|
||||
InputStream input = classLoader.getResourceAsStream("logging.properties");
|
||||
if (input == null) {
|
||||
Path javaHome = Paths.get(System.getProperty("java.home"));
|
||||
input = Files.newInputStream(javaHome.resolve("lib").resolve("logging.properties"));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package net.corda.demobench.pty;
|
||||
|
||||
import com.jediterm.terminal.ProcessTtyConnector;
|
||||
import com.pty4j.PtyProcess;
|
||||
import com.pty4j.WinSize;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Copied from JediTerm pty.
|
||||
* JediTerm is not available in any Maven repository.
|
||||
* @author traff
|
||||
*/
|
||||
public class PtyProcessTtyConnector extends ProcessTtyConnector {
|
||||
private final PtyProcess myProcess;
|
||||
private final String name;
|
||||
|
||||
PtyProcessTtyConnector(String name, PtyProcess process, Charset charset) {
|
||||
super(process, charset);
|
||||
myProcess = process;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void resizeImmediately() {
|
||||
if (getPendingTermSize() != null && getPendingPixelSize() != null) {
|
||||
myProcess.setWinSize(
|
||||
new WinSize(getPendingTermSize().width, getPendingTermSize().height, getPendingPixelSize().width, getPendingPixelSize().height));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return myProcess.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
package net.corda.demobench
|
||||
|
||||
import javafx.scene.image.Image
|
||||
import net.corda.demobench.views.DemoBenchView
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import tornadofx.App
|
||||
import tornadofx.addStageIcon
|
||||
|
||||
/**
|
||||
* README!
|
||||
@ -32,6 +37,22 @@ import tornadofx.App
|
||||
*/
|
||||
|
||||
class DemoBench : App(DemoBenchView::class) {
|
||||
init {
|
||||
|
||||
/*
|
||||
* This entry point is needed by JavaPackager, as
|
||||
* otherwise the packaged application cannot run.
|
||||
*/
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) = launch(DemoBench::class.java, *args)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
addStageIcon(Image("cordalogo.png"))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Trivial utility function to create SLF4J Logger.
|
||||
*/
|
||||
inline fun <reified T: Any> loggerFor(): Logger = LoggerFactory.getLogger(T::class.java)
|
||||
|
@ -1,13 +0,0 @@
|
||||
package net.corda.demobench
|
||||
|
||||
import javafx.scene.Parent
|
||||
import tornadofx.View
|
||||
import tornadofx.importStylesheet
|
||||
|
||||
class DemoBenchView : View("Corda Demo Bench") {
|
||||
override val root: Parent by fxml()
|
||||
|
||||
init {
|
||||
importStylesheet("/net/corda/demobench/style.css")
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import net.corda.demobench.loggerFor
|
||||
import org.h2.server.web.LocalWebServer
|
||||
import org.h2.tools.Server
|
||||
import org.h2.util.JdbcUtils
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
class DBViewer : AutoCloseable {
|
||||
private companion object {
|
||||
val log = loggerFor<DBViewer>()
|
||||
}
|
||||
|
||||
private val webServer: Server
|
||||
private val pool = Executors.newCachedThreadPool()
|
||||
|
||||
init {
|
||||
val ws = LocalWebServer()
|
||||
webServer = Server(ws, "-webPort", "0")
|
||||
ws.setShutdownHandler(webServer)
|
||||
|
||||
webServer.setShutdownHandler {
|
||||
webServer.stop()
|
||||
}
|
||||
|
||||
pool.submit {
|
||||
webServer.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
log.info("Shutting down")
|
||||
pool.shutdown()
|
||||
webServer.shutdown()
|
||||
}
|
||||
|
||||
fun openBrowser(h2Port: Int) {
|
||||
val conn = JdbcUtils.getConnection(
|
||||
org.h2.Driver::class.jvmName,
|
||||
"jdbc:h2:tcp://localhost:$h2Port/node",
|
||||
"sa",
|
||||
""
|
||||
)
|
||||
|
||||
val url = (webServer.service as LocalWebServer).addSession(conn)
|
||||
log.info("Session: {}", url)
|
||||
|
||||
pool.execute {
|
||||
Server.openBrowser(url)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import net.corda.demobench.loggerFor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class Explorer(val explorerController: ExplorerController) : AutoCloseable {
|
||||
private companion object {
|
||||
val log = loggerFor<Explorer>()
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private var process: Process? = null
|
||||
|
||||
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
|
||||
val explorerDir = config.explorerDir.toFile()
|
||||
|
||||
if (!explorerDir.isDirectory && !explorerDir.mkdirs()) {
|
||||
log.warn("Failed to create working directory '{}'", explorerDir.absolutePath)
|
||||
onExit(config)
|
||||
return
|
||||
}
|
||||
|
||||
val p = explorerController.process(
|
||||
"--host=localhost",
|
||||
"--port=${config.artemisPort}",
|
||||
"--username=${config.users[0]["user"]}",
|
||||
"--password=${config.users[0]["password"]}",
|
||||
"--certificatesDir=${config.ssl.certificatesDirectory}",
|
||||
"--keyStorePassword=${config.ssl.keyStorePassword}",
|
||||
"--trustStorePassword=${config.ssl.trustStorePassword}")
|
||||
.directory(explorerDir)
|
||||
.start()
|
||||
process = p
|
||||
|
||||
log.info("Launched Node Explorer for '{}'", config.legalName)
|
||||
|
||||
// Close these streams because no-one is using them.
|
||||
safeClose(p.outputStream)
|
||||
safeClose(p.inputStream)
|
||||
safeClose(p.errorStream)
|
||||
|
||||
executor.submit {
|
||||
val exitValue = p.waitFor()
|
||||
process = null
|
||||
|
||||
log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue)
|
||||
onExit(config)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
executor.shutdown()
|
||||
process?.destroy()
|
||||
}
|
||||
|
||||
private fun safeClose(c: AutoCloseable) {
|
||||
try {
|
||||
c.close()
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to close stream: '{}'", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.Controller
|
||||
|
||||
class ExplorerController : Controller() {
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
private val explorerPath = jvm.applicationDir.resolve("explorer").resolve("node-explorer.jar")
|
||||
|
||||
init {
|
||||
log.info("Explorer JAR: $explorerPath")
|
||||
}
|
||||
|
||||
internal fun process(vararg args: String) = jvm.processFor(explorerPath, *args)
|
||||
|
||||
fun explorer() = Explorer(this)
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import tornadofx.Controller
|
||||
|
||||
class JVMConfig : Controller() {
|
||||
|
||||
val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath()
|
||||
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
|
||||
val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath()
|
||||
|
||||
init {
|
||||
log.info("Java executable: $javaPath")
|
||||
}
|
||||
|
||||
fun commandFor(jarPath: Path, vararg args: String): Array<String> {
|
||||
return arrayOf(javaPath.toString(), "-jar", jarPath.toString(), *args)
|
||||
}
|
||||
|
||||
fun processFor(jarPath: Path, vararg args: String): ProcessBuilder {
|
||||
return ProcessBuilder(commandFor(jarPath, *args).toList())
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
open class NetworkMapConfig(val legalName: String, val artemisPort: Int) {
|
||||
|
||||
private var keyValue = toKey(legalName)
|
||||
val key: String get() = keyValue
|
||||
|
||||
}
|
||||
|
||||
private val WHITESPACE = "\\s++".toRegex()
|
||||
|
||||
fun toKey(value: String) = value.replace(WHITESPACE, "").toLowerCase()
|
@ -0,0 +1,82 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import com.typesafe.config.*
|
||||
import java.lang.String.join
|
||||
import java.nio.file.Path
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
|
||||
class NodeConfig(
|
||||
baseDir: Path,
|
||||
legalName: String,
|
||||
artemisPort: Int,
|
||||
val nearestCity: String,
|
||||
val webPort: Int,
|
||||
val h2Port: Int,
|
||||
val extraServices: List<String>,
|
||||
val users: List<Map<String, Any>> = listOf(defaultUser),
|
||||
var networkMap: NetworkMapConfig? = null
|
||||
) : NetworkMapConfig(legalName, artemisPort) {
|
||||
|
||||
companion object {
|
||||
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||
|
||||
val defaultUser: Map<String, Any> = mapOf(
|
||||
"user" to "guest",
|
||||
"password" to "letmein",
|
||||
"permissions" to listOf(
|
||||
"StartFlow.net.corda.flows.CashFlow",
|
||||
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val nodeDir: Path = baseDir.resolve(key)
|
||||
val explorerDir: Path = baseDir.resolve("$key-explorer")
|
||||
|
||||
val ssl: SSLConfiguration = object : SSLConfiguration {
|
||||
override val certificatesDirectory: Path = nodeDir.resolve("certificates")
|
||||
override val trustStorePassword: String = "trustpass"
|
||||
override val keyStorePassword: String = "cordacadevpass"
|
||||
}
|
||||
|
||||
var state: NodeState = NodeState.STARTING
|
||||
|
||||
val isCashIssuer: Boolean = extraServices.any {
|
||||
it.startsWith("corda.issuer.")
|
||||
}
|
||||
|
||||
fun isNetworkMap(): Boolean = networkMap == null
|
||||
|
||||
/*
|
||||
* The configuration object depends upon the networkMap,
|
||||
* which is mutable.
|
||||
*/
|
||||
fun toFileConfig(): Config = ConfigFactory.empty()
|
||||
.withValue("myLegalName", valueFor(legalName))
|
||||
.withValue("artemisAddress", addressValueFor(artemisPort))
|
||||
.withValue("nearestCity", valueFor(nearestCity))
|
||||
.withValue("extraAdvertisedServiceIds", valueFor(join(",", extraServices)))
|
||||
.withFallback(optional("networkMapService", networkMap, {
|
||||
c, n -> c.withValue("address", addressValueFor(n.artemisPort))
|
||||
.withValue("legalName", valueFor(n.legalName))
|
||||
} ))
|
||||
.withValue("webAddress", addressValueFor(webPort))
|
||||
.withValue("rpcUsers", valueFor(users))
|
||||
.withValue("h2port", valueFor(h2Port))
|
||||
.withValue("useTestClock", valueFor(true))
|
||||
|
||||
fun toText(): String = toFileConfig().root().render(renderOptions)
|
||||
|
||||
fun moveTo(baseDir: Path) = NodeConfig(
|
||||
baseDir, legalName, artemisPort, nearestCity, webPort, h2Port, extraServices, users, networkMap
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
|
||||
|
||||
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
|
||||
|
||||
private fun <T> optional(path: String, obj: T?, body: (Config, T) -> Config): Config {
|
||||
val config = ConfigFactory.empty()
|
||||
return if (obj == null) config else body(config, obj).atPath(path)
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import java.io.IOException
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.ServerSocket
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import net.corda.demobench.pty.R3Pty
|
||||
import tornadofx.Controller
|
||||
|
||||
class NodeController : Controller() {
|
||||
private companion object {
|
||||
const val firstPort = 10000
|
||||
const val minPort = 1024
|
||||
const val maxPort = 65535
|
||||
}
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
|
||||
private var baseDir = baseDirFor(ManagementFactory.getRuntimeMXBean().startTime)
|
||||
private val pluginDir = jvm.applicationDir.resolve("plugins")
|
||||
|
||||
private val bankOfCorda = pluginDir.resolve("bank-of-corda.jar").toFile()
|
||||
|
||||
private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
|
||||
private val command = jvm.commandFor(cordaPath)
|
||||
|
||||
private val nodes = LinkedHashMap<String, NodeConfig>()
|
||||
private val port = AtomicInteger(firstPort)
|
||||
|
||||
private var networkMapConfig: NetworkMapConfig? = null
|
||||
|
||||
val activeNodes: List<NodeConfig> get() = nodes.values.filter {
|
||||
(it.state == NodeState.RUNNING) || (it.state == NodeState.STARTING)
|
||||
}
|
||||
|
||||
init {
|
||||
log.info("Base directory: $baseDir")
|
||||
log.info("Corda JAR: $cordaPath")
|
||||
}
|
||||
|
||||
fun validate(nodeData: NodeData): NodeConfig? {
|
||||
val config = NodeConfig(
|
||||
baseDir,
|
||||
nodeData.legalName.value.trim(),
|
||||
nodeData.artemisPort.value,
|
||||
nodeData.nearestCity.value.trim(),
|
||||
nodeData.webPort.value,
|
||||
nodeData.h2Port.value,
|
||||
nodeData.extraServices.value
|
||||
)
|
||||
|
||||
if (nodes.putIfAbsent(config.key, config) != null) {
|
||||
log.warning("Node with key '${config.key}' already exists.")
|
||||
return null
|
||||
}
|
||||
|
||||
// The first node becomes our network map
|
||||
chooseNetworkMap(config)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun dispose(config: NodeConfig) {
|
||||
config.state = NodeState.DEAD
|
||||
|
||||
if (config.networkMap == null) {
|
||||
log.warning("Network map service (Node '${config.legalName}') has exited.")
|
||||
}
|
||||
}
|
||||
|
||||
val nextPort: Int get() = port.andIncrement
|
||||
|
||||
fun isPortAvailable(port: Int): Boolean {
|
||||
if (isPortValid(port)) {
|
||||
try {
|
||||
ServerSocket(port).close()
|
||||
return true
|
||||
} catch (e: IOException) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun isPortValid(port: Int): Boolean = (port >= minPort) && (port <= maxPort)
|
||||
|
||||
fun keyExists(key: String) = nodes.keys.contains(key)
|
||||
|
||||
fun nameExists(name: String) = keyExists(toKey(name))
|
||||
|
||||
fun hasNetworkMap(): Boolean = networkMapConfig != null
|
||||
|
||||
fun chooseNetworkMap(config: NodeConfig) {
|
||||
if (hasNetworkMap()) {
|
||||
config.networkMap = networkMapConfig
|
||||
} else {
|
||||
networkMapConfig = config
|
||||
log.info("Network map provided by: ${config.legalName}")
|
||||
}
|
||||
}
|
||||
|
||||
fun runCorda(pty: R3Pty, config: NodeConfig): Boolean {
|
||||
val nodeDir = config.nodeDir.toFile()
|
||||
|
||||
if (nodeDir.isDirectory || nodeDir.mkdirs()) {
|
||||
try {
|
||||
// Write this node's configuration file into its working directory.
|
||||
val confFile = nodeDir.resolve("node.conf")
|
||||
confFile.writeText(config.toText())
|
||||
|
||||
// Nodes cannot issue cash unless they contain the "Bank of Corda" plugin.
|
||||
if (config.isCashIssuer && bankOfCorda.isFile) {
|
||||
log.info("Installing 'Bank of Corda' plugin")
|
||||
bankOfCorda.copyTo(nodeDir.resolve("plugins").resolve(bankOfCorda.name), overwrite=true)
|
||||
}
|
||||
|
||||
// Execute the Corda node
|
||||
pty.run(command, System.getenv(), nodeDir.toString())
|
||||
log.info("Launched node: ${config.legalName}")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
log.severe("Failed to launch Corda:" + e)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
baseDir = baseDirFor(System.currentTimeMillis())
|
||||
log.info("Changed base directory: $baseDir")
|
||||
|
||||
// Wipe out any knowledge of previous nodes.
|
||||
networkMapConfig = null
|
||||
nodes.clear()
|
||||
}
|
||||
|
||||
fun register(config: NodeConfig): Boolean {
|
||||
if (nodes.putIfAbsent(config.key, config) != null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ((networkMapConfig == null) && config.isNetworkMap()) {
|
||||
networkMapConfig = config
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun relocate(config: NodeConfig) = config.moveTo(baseDir)
|
||||
|
||||
private fun baseDirFor(time: Long) = jvm.userHome.resolve("demobench").resolve(localFor(time))
|
||||
private fun localFor(time: Long) = SimpleDateFormat("yyyyMMddHHmmss").format(Date(time))
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.observable
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleListProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
|
||||
class NodeData {
|
||||
|
||||
val legalName = SimpleStringProperty("")
|
||||
val nearestCity = SimpleStringProperty("London")
|
||||
val artemisPort = SimpleIntegerProperty()
|
||||
val webPort = SimpleIntegerProperty()
|
||||
val h2Port = SimpleIntegerProperty()
|
||||
val extraServices = SimpleListProperty(mutableListOf<String>().observable())
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.ItemViewModel
|
||||
|
||||
class NodeDataModel : ItemViewModel<NodeData>(NodeData()) {
|
||||
|
||||
val legalName = bind { item?.legalName }
|
||||
val nearestCity = bind { item?.nearestCity }
|
||||
val artemisPort = bind { item?.artemisPort }
|
||||
val webPort = bind { item?.webPort }
|
||||
val h2Port = bind { item?.h2Port }
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
enum class NodeState {
|
||||
STARTING,
|
||||
RUNNING,
|
||||
DEAD
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.Controller
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
class ServiceController : Controller() {
|
||||
|
||||
val services: List<String> = loadConf(javaClass.classLoader.getResource("services.conf"))
|
||||
|
||||
val notaries: List<String> = services.filter { it.startsWith("corda.notary.") }.toList()
|
||||
|
||||
/*
|
||||
* Load our list of known extra Corda services.
|
||||
*/
|
||||
private fun loadConf(url: URL?): List<String> {
|
||||
if (url == null) {
|
||||
return emptyList()
|
||||
} else {
|
||||
val set = TreeSet<String>()
|
||||
InputStreamReader(url.openStream()).useLines { sq ->
|
||||
sq.forEach { line ->
|
||||
val service = line.trim()
|
||||
set.add(service)
|
||||
|
||||
log.info("Supports: $service")
|
||||
}
|
||||
}
|
||||
return set.toList()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import net.corda.demobench.loggerFor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class WebServer(val webServerController: WebServerController) : AutoCloseable {
|
||||
private companion object {
|
||||
val log = loggerFor<WebServer>()
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
private var process: Process? = null
|
||||
|
||||
fun open(config: NodeConfig, onExit: (NodeConfig) -> Unit) {
|
||||
val nodeDir = config.nodeDir.toFile()
|
||||
|
||||
if (!nodeDir.isDirectory) {
|
||||
log.warn("Working directory '{}' does not exist.", nodeDir.absolutePath)
|
||||
onExit(config)
|
||||
return
|
||||
}
|
||||
|
||||
val p = webServerController.process()
|
||||
.directory(nodeDir)
|
||||
.start()
|
||||
process = p
|
||||
|
||||
log.info("Launched Web Server for '{}'", config.legalName)
|
||||
|
||||
// Close these streams because no-one is using them.
|
||||
safeClose(p.outputStream)
|
||||
safeClose(p.inputStream)
|
||||
safeClose(p.errorStream)
|
||||
|
||||
executor.submit {
|
||||
val exitValue = p.waitFor()
|
||||
process = null
|
||||
|
||||
log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue)
|
||||
onExit(config)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
executor.shutdown()
|
||||
process?.destroy()
|
||||
}
|
||||
|
||||
private fun safeClose(c: AutoCloseable) {
|
||||
try {
|
||||
c.close()
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to close stream: '{}'", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import tornadofx.Controller
|
||||
|
||||
class WebServerController : Controller() {
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
private val cordaPath = jvm.applicationDir.resolve("corda").resolve("corda.jar")
|
||||
|
||||
init {
|
||||
log.info("Web Server JAR: $cordaPath")
|
||||
}
|
||||
|
||||
internal fun process() = jvm.processFor(cordaPath, "--webserver")
|
||||
|
||||
fun webServer() = WebServer(this)
|
||||
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package net.corda.demobench.profile
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.nio.file.*
|
||||
import java.util.*
|
||||
import java.util.function.BiPredicate
|
||||
import java.util.stream.StreamSupport
|
||||
import javafx.stage.FileChooser
|
||||
import javafx.stage.FileChooser.ExtensionFilter
|
||||
import kotlinx.support.jdk8.collections.spliterator
|
||||
import net.corda.demobench.model.*
|
||||
import tornadofx.Controller
|
||||
|
||||
class ProfileController : Controller() {
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
private val baseDir = jvm.userHome.resolve("demobench")
|
||||
private val nodeController by inject<NodeController>()
|
||||
private val serviceController by inject<ServiceController>()
|
||||
private val chooser = FileChooser()
|
||||
|
||||
init {
|
||||
chooser.title = "DemoBench Profiles"
|
||||
chooser.initialDirectory = baseDir.toFile()
|
||||
chooser.extensionFilters.add(ExtensionFilter("DemoBench profiles (*.zip)", "*.zip", "*.ZIP"))
|
||||
}
|
||||
|
||||
fun saveProfile(): Boolean {
|
||||
val target = forceExtension(chooser.showSaveDialog(null) ?: return false, ".zip")
|
||||
log.info("Save profile as: $target")
|
||||
|
||||
val configs = nodeController.activeNodes
|
||||
|
||||
// Delete the profile, if it already exists. The save
|
||||
// dialogue has already confirmed that this is OK.
|
||||
target.delete()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + target.toURI()), mapOf("create" to "true")).use { fs ->
|
||||
configs.forEach { config ->
|
||||
val nodeDir = Files.createDirectories(fs.getPath(config.key))
|
||||
val file = Files.write(nodeDir.resolve("node.conf"), config.toText().toByteArray(UTF_8))
|
||||
log.info("Wrote: $file")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun forceExtension(target: File, ext: String): File {
|
||||
return if (target.extension.isEmpty()) File(target.parent, target.name + ext) else target
|
||||
}
|
||||
|
||||
fun openProfile(): List<NodeConfig>? {
|
||||
val chosen = chooser.showOpenDialog(null) ?: return null
|
||||
log.info("Selected profile: $chosen")
|
||||
|
||||
val configs = LinkedList<NodeConfig>()
|
||||
|
||||
FileSystems.newFileSystem(chosen.toPath(), null).use { fs ->
|
||||
StreamSupport.stream(fs.rootDirectories.spliterator(), false)
|
||||
.flatMap { Files.find(it, 2, BiPredicate { p, attr -> "node.conf" == p?.fileName.toString() }) }
|
||||
.forEach { file ->
|
||||
try {
|
||||
// Java seems to "walk" through the ZIP file backwards.
|
||||
// So add new config to the front of the list, so that
|
||||
// our final list is ordered to match the file.
|
||||
configs.addFirst(toNodeConfig(parse(file)))
|
||||
log.info("Loaded: $file")
|
||||
} catch (e: Exception) {
|
||||
log.severe("Failed to parse '$file': ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
private fun toNodeConfig(config: Config): NodeConfig {
|
||||
val artemisPort = config.parsePort("artemisAddress")
|
||||
val webPort = config.parsePort("webAddress")
|
||||
val h2Port = config.getInt("h2port")
|
||||
val extraServices = config.parseExtraServices("extraAdvertisedServiceIds")
|
||||
|
||||
val nodeConfig = NodeConfig(
|
||||
baseDir, // temporary value
|
||||
config.getString("myLegalName"),
|
||||
artemisPort,
|
||||
config.getString("nearestCity"),
|
||||
webPort,
|
||||
h2Port,
|
||||
extraServices,
|
||||
config.getObjectList("rpcUsers").map { it.unwrapped() }.toList()
|
||||
)
|
||||
|
||||
if (config.hasPath("networkMapService")) {
|
||||
val nmap = config.getConfig("networkMapService")
|
||||
nodeConfig.networkMap = NetworkMapConfig(nmap.getString("legalName"), nmap.parsePort("address"))
|
||||
} else {
|
||||
log.info("Node '${nodeConfig.legalName}' is the network map")
|
||||
}
|
||||
|
||||
return nodeConfig
|
||||
}
|
||||
|
||||
private fun parse(path: Path): Config = Files.newBufferedReader(path).use {
|
||||
return ConfigFactory.parseReader(it)
|
||||
}
|
||||
|
||||
private fun Config.parsePort(path: String): Int {
|
||||
val address = this.getString(path)
|
||||
val port = HostAndPort.fromString(address).port
|
||||
require(nodeController.isPortValid(port), { "Invalid port $port from '$path'." })
|
||||
return port
|
||||
}
|
||||
|
||||
private fun Config.parseExtraServices(path: String): List<String> {
|
||||
val services = serviceController.services.toSortedSet()
|
||||
return this.getString(path).split(",")
|
||||
.filter { it -> !it.isNullOrEmpty() }
|
||||
.map { svc ->
|
||||
require(svc in services, { "Unknown service '$svc'." } )
|
||||
svc
|
||||
}.toList()
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package net.corda.demobench.pty
|
||||
|
||||
import com.jediterm.terminal.TtyConnector
|
||||
import com.jediterm.terminal.ui.*
|
||||
import com.jediterm.terminal.ui.settings.SettingsProvider
|
||||
import com.pty4j.PtyProcess
|
||||
import net.corda.demobench.loggerFor
|
||||
|
||||
import java.awt.*
|
||||
import java.nio.charset.StandardCharsets.UTF_8
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension, val onExit: () -> Unit) : AutoCloseable {
|
||||
private companion object {
|
||||
val log = loggerFor<R3Pty>()
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
val terminal = JediTermWidget(dimension, settings)
|
||||
|
||||
override fun close() {
|
||||
log.info("Closing terminal '{}'", name)
|
||||
executor.shutdown()
|
||||
terminal.close()
|
||||
}
|
||||
|
||||
private fun createTtyConnector(command: Array<String>, environment: Map<String, String>, workingDir: String?): TtyConnector {
|
||||
val process = PtyProcess.exec(command, environment, workingDir)
|
||||
|
||||
try {
|
||||
return PtyProcessTtyConnector(name, process, UTF_8)
|
||||
} catch (e: Exception) {
|
||||
process.destroyForcibly()
|
||||
process.waitFor(30, TimeUnit.SECONDS)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun run(args: Array<String>, envs: Map<String, String>, workingDir: String?) {
|
||||
check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" })
|
||||
|
||||
val environment = HashMap<String, String>(envs)
|
||||
if (!UIUtil.isWindows) {
|
||||
environment["TERM"] = "xterm"
|
||||
}
|
||||
|
||||
val connector = createTtyConnector(args, environment, workingDir)
|
||||
|
||||
executor.submit {
|
||||
val exitValue = connector.waitFor()
|
||||
log.info("Terminal has exited (value={})", exitValue)
|
||||
onExit()
|
||||
}
|
||||
|
||||
val session = terminal.createTerminalSession(connector)
|
||||
session.start()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.corda.demobench.rpc
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.demobench.loggerFor
|
||||
import net.corda.demobench.model.NodeConfig
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
|
||||
class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Unit): AutoCloseable {
|
||||
|
||||
private companion object {
|
||||
val log = loggerFor<NodeRPC>()
|
||||
val oneSecond = SECONDS.toMillis(1)
|
||||
}
|
||||
|
||||
private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.artemisPort), config.ssl)
|
||||
private val timer = Timer()
|
||||
|
||||
init {
|
||||
val setupTask = object : TimerTask() {
|
||||
override fun run() {
|
||||
try {
|
||||
rpcClient.start(config.users[0].getOrElse("user") { "none" } as String,
|
||||
config.users[0].getOrElse("password") { "none" } as String)
|
||||
val ops = rpcClient.proxy()
|
||||
|
||||
// Cancel the "setup" task now that we've created the RPC client.
|
||||
this.cancel()
|
||||
|
||||
// Run "start-up" task, now that the RPC client is ready.
|
||||
start()
|
||||
|
||||
// Schedule a new task that will refresh the display once per second.
|
||||
timer.schedule(object: TimerTask() {
|
||||
override fun run() {
|
||||
invoke(ops)
|
||||
}
|
||||
}, 0, oneSecond)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Node '{}' not ready yet (Error: {})", config.legalName, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 5 seconds for the node to start, and then poll once per second.
|
||||
timer.schedule(setupTask, 5 * oneSecond, oneSecond)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
timer.cancel()
|
||||
rpcClient.close()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.corda.demobench.ui
|
||||
|
||||
import com.sun.javafx.scene.control.behavior.TabPaneBehavior
|
||||
import com.sun.javafx.scene.control.skin.TabPaneSkin
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.Tab
|
||||
|
||||
class CloseableTab(text: String, content: Node) : Tab(text, content) {
|
||||
|
||||
fun requestClose() {
|
||||
val b = behaviour
|
||||
if ((b != null) && b.canCloseTab(this)) {
|
||||
b.closeTab(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val behaviour: TabPaneBehavior?
|
||||
get() = (tabPane?.skin as? TabPaneSkin)?.behavior
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.corda.demobench.ui
|
||||
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.layout.HBox
|
||||
|
||||
class PropertyLabel() : HBox() {
|
||||
|
||||
val nameLabel = Label()
|
||||
val valueLabel = Label()
|
||||
|
||||
var name : String
|
||||
get() = nameLabel.text
|
||||
set(value) {
|
||||
nameLabel.text = value
|
||||
}
|
||||
|
||||
var value: String
|
||||
get() = valueLabel.text
|
||||
set(value) {
|
||||
valueLabel.text = value
|
||||
}
|
||||
|
||||
init {
|
||||
nameLabel.styleClass.add("property-name")
|
||||
valueLabel.styleClass.add("property-value")
|
||||
|
||||
children.addAll(nameLabel, valueLabel)
|
||||
styleClass.add("property-label")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package net.corda.demobench.views
|
||||
|
||||
import java.util.*
|
||||
import javafx.application.Platform
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.Button
|
||||
import javafx.scene.control.MenuItem
|
||||
import javafx.scene.control.Tab
|
||||
import javafx.scene.control.TabPane
|
||||
import net.corda.demobench.model.NodeConfig
|
||||
import net.corda.demobench.model.NodeController
|
||||
import net.corda.demobench.profile.ProfileController
|
||||
import net.corda.demobench.ui.CloseableTab
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.*
|
||||
|
||||
class DemoBenchView : View("Corda Demo Bench") {
|
||||
|
||||
override val root by fxml<Parent>()
|
||||
|
||||
private val profileController by inject<ProfileController>()
|
||||
private val nodeController by inject<NodeController>()
|
||||
private val addNodeButton by fxid<Button>()
|
||||
private val nodeTabPane by fxid<TabPane>()
|
||||
private val menuOpen by fxid<MenuItem>()
|
||||
private val menuSaveAs by fxid<MenuItem>()
|
||||
|
||||
init {
|
||||
importStylesheet("/net/corda/demobench/style.css")
|
||||
|
||||
configureShutdown()
|
||||
|
||||
configureProfileSaveAs()
|
||||
configureProfileOpen()
|
||||
|
||||
configureAddNode()
|
||||
}
|
||||
|
||||
private fun configureShutdown() = primaryStage.setOnCloseRequest {
|
||||
log.info("Exiting")
|
||||
|
||||
// Prevent any new NodeTabViews from being created.
|
||||
addNodeButton.isDisable = true
|
||||
|
||||
closeAllTabs()
|
||||
Platform.exit()
|
||||
}
|
||||
|
||||
private fun configureProfileSaveAs() = menuSaveAs.setOnAction {
|
||||
try {
|
||||
if (profileController.saveProfile()) {
|
||||
menuSaveAs.isDisable = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureProfileOpen() = menuOpen.setOnAction {
|
||||
try {
|
||||
val profile = profileController.openProfile()
|
||||
if (profile != null) {
|
||||
loadProfile(profile)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureAddNode() {
|
||||
addNodeButton.setOnAction {
|
||||
val nodeTabView = createNodeTabView(true)
|
||||
nodeTabPane.selectionModel.select(nodeTabView.nodeTab)
|
||||
|
||||
// Prevent us from creating new nodes until we have created the Network Map
|
||||
addNodeButton.isDisable = true
|
||||
}
|
||||
addNodeButton.fire()
|
||||
}
|
||||
|
||||
private fun closeAllTabs() = ArrayList<Tab>(nodeTabPane.tabs).forEach {
|
||||
(it as CloseableTab).requestClose()
|
||||
}
|
||||
|
||||
private fun createNodeTabView(showConfig: Boolean): NodeTabView {
|
||||
val nodeTabView = find<NodeTabView>(mapOf("showConfig" to showConfig))
|
||||
nodeTabPane.tabs.add(nodeTabView.nodeTab)
|
||||
return nodeTabView
|
||||
}
|
||||
|
||||
private fun loadProfile(nodes: List<NodeConfig>) {
|
||||
closeAllTabs()
|
||||
nodeController.reset()
|
||||
|
||||
nodes.forEach {
|
||||
val nodeTabView = createNodeTabView(false)
|
||||
nodeTabView.launch(nodeController.relocate(it))
|
||||
}
|
||||
|
||||
enableAddNodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the "save profile" menu item.
|
||||
*/
|
||||
fun enableSaveProfile() {
|
||||
menuSaveAs.isDisable = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the button that allows us to create a new node.
|
||||
*/
|
||||
fun enableAddNodes() {
|
||||
addNodeButton.isDisable = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that DemoBench always has at least one instance NodeTabView.
|
||||
* This method must NOT be called if DemoBench is shutting down.
|
||||
*/
|
||||
fun forceAtLeastOneTab() {
|
||||
if (nodeTabPane.tabs.isEmpty()) {
|
||||
addNodeButton.fire()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package net.corda.demobench.views
|
||||
|
||||
import java.text.DecimalFormat
|
||||
import javafx.application.Platform
|
||||
import javafx.scene.control.SelectionMode.MULTIPLE
|
||||
import javafx.scene.layout.Pane
|
||||
import javafx.util.converter.NumberStringConverter
|
||||
import net.corda.demobench.model.NodeConfig
|
||||
import net.corda.demobench.model.NodeController
|
||||
import net.corda.demobench.model.NodeDataModel
|
||||
import net.corda.demobench.model.ServiceController
|
||||
import net.corda.demobench.ui.CloseableTab
|
||||
import tornadofx.*
|
||||
|
||||
class NodeTabView : Fragment() {
|
||||
override val root = stackpane {}
|
||||
|
||||
private val main by inject<DemoBenchView>()
|
||||
private val showConfig by param<Boolean>()
|
||||
|
||||
private companion object {
|
||||
val integerFormat = DecimalFormat()
|
||||
val notNumber = "[^\\d]".toRegex()
|
||||
}
|
||||
|
||||
private val model = NodeDataModel()
|
||||
private val nodeController by inject<NodeController>()
|
||||
private val serviceController by inject<ServiceController>()
|
||||
|
||||
private val nodeTerminalView = find<NodeTerminalView>()
|
||||
private val nodeConfigView = stackpane {
|
||||
isVisible = showConfig
|
||||
|
||||
form {
|
||||
fieldset("Configuration") {
|
||||
field("Node Name", op = { nodeNameField() })
|
||||
field("Nearest City", op = { nearestCityField() })
|
||||
field("P2P Port", op = { p2pPortField() })
|
||||
field("Web Port", op = { webPortField() })
|
||||
field("Database Port", op = { databasePortField() })
|
||||
}
|
||||
|
||||
fieldset("Services") {
|
||||
listview(availableServices.observable()) {
|
||||
selectionModel.selectionMode = MULTIPLE
|
||||
model.item.extraServices.set(selectionModel.selectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
button("Create Node") {
|
||||
setOnAction {
|
||||
if (model.validate()) {
|
||||
launch()
|
||||
main.enableAddNodes()
|
||||
main.enableSaveProfile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nodeTab = CloseableTab("New Node", root)
|
||||
|
||||
private val availableServices: List<String>
|
||||
get() = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries
|
||||
|
||||
init {
|
||||
integerFormat.isGroupingUsed = false
|
||||
|
||||
// Ensure that we destroy the terminal along with the tab.
|
||||
nodeTab.setOnCloseRequest {
|
||||
nodeTerminalView.destroy()
|
||||
}
|
||||
|
||||
root.add(nodeConfigView)
|
||||
root.add(nodeTerminalView)
|
||||
|
||||
model.artemisPort.value = nodeController.nextPort
|
||||
model.webPort.value = nodeController.nextPort
|
||||
model.h2Port.value = nodeController.nextPort
|
||||
}
|
||||
|
||||
private fun Pane.nodeNameField() = textfield(model.legalName) {
|
||||
minWidth = 200.0
|
||||
maxWidth = 200.0
|
||||
validator {
|
||||
if (it == null) {
|
||||
error("Node name is required")
|
||||
} else {
|
||||
val name = it.trim()
|
||||
if (name.isEmpty()) {
|
||||
error("Node name is required")
|
||||
} else if (nodeController.nameExists(name)) {
|
||||
error("Node with this name already exists")
|
||||
} else if (name.length > 10) {
|
||||
error("Name is too long")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.nearestCityField() = textfield(model.nearestCity) {
|
||||
minWidth = 200.0
|
||||
maxWidth = 200.0
|
||||
validator {
|
||||
if (it == null) {
|
||||
error("Nearest city is required")
|
||||
} else if (it.trim().isEmpty()) {
|
||||
error("Nearest city is required")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.p2pPortField() = textfield(model.artemisPort, NumberStringConverter(integerFormat)) {
|
||||
minWidth = 100.0
|
||||
maxWidth = 100.0
|
||||
validator {
|
||||
if ((it == null) || it.isEmpty()) {
|
||||
error("Port number required")
|
||||
} else if (it.contains(notNumber)) {
|
||||
error("Invalid port number")
|
||||
} else {
|
||||
val port = it.toInt()
|
||||
if (!nodeController.isPortAvailable(port)) {
|
||||
error("Port $it is unavailable")
|
||||
} else if (port == model.webPort.value) {
|
||||
error("Clashes with web port")
|
||||
} else if (port == model.h2Port.value) {
|
||||
error("Clashes with database port")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.webPortField() = textfield(model.webPort, NumberStringConverter(integerFormat)) {
|
||||
minWidth = 100.0
|
||||
maxWidth = 100.0
|
||||
validator {
|
||||
if ((it == null) || it.isEmpty()) {
|
||||
error("Port number required")
|
||||
} else if (it.contains(notNumber)) {
|
||||
error("Invalid port number")
|
||||
} else {
|
||||
val port = it.toInt()
|
||||
if (!nodeController.isPortAvailable(port)) {
|
||||
error("Port $it is unavailable")
|
||||
} else if (port == model.artemisPort.value) {
|
||||
error("Clashes with P2P port")
|
||||
} else if (port == model.h2Port.value) {
|
||||
error("Clashes with database port")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Pane.databasePortField() = textfield(model.h2Port, NumberStringConverter(integerFormat)) {
|
||||
minWidth = 100.0
|
||||
maxWidth = 100.0
|
||||
validator {
|
||||
if ((it == null) || it.isEmpty()) {
|
||||
error("Port number required")
|
||||
} else if (it.contains(notNumber)) {
|
||||
error("Invalid port number")
|
||||
} else {
|
||||
val port = it.toInt()
|
||||
if (!nodeController.isPortAvailable(port)) {
|
||||
error("Port $it is unavailable")
|
||||
} else if (port == model.artemisPort.value) {
|
||||
error("Clashes with P2P port")
|
||||
} else if (port == model.webPort.value) {
|
||||
error("Clashes with web port")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a Corda node that was configured via the form.
|
||||
*/
|
||||
fun launch() {
|
||||
model.commit()
|
||||
val config = nodeController.validate(model.item)
|
||||
if (config != null) {
|
||||
nodeConfigView.isVisible = false
|
||||
launchNode(config)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches a preconfigured Corda node, e.g. from a saved profile.
|
||||
*/
|
||||
fun launch(config: NodeConfig) {
|
||||
nodeController.register(config)
|
||||
launchNode(config)
|
||||
}
|
||||
|
||||
private fun launchNode(config: NodeConfig) {
|
||||
nodeTab.text = config.legalName
|
||||
nodeTerminalView.open(config, onExit = { onTerminalExit(config) })
|
||||
|
||||
nodeTab.setOnSelectionChanged {
|
||||
if (nodeTab.isSelected) {
|
||||
// Doesn't work yet
|
||||
nodeTerminalView.refreshTerminal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTerminalExit(config: NodeConfig) {
|
||||
Platform.runLater {
|
||||
nodeTab.requestClose()
|
||||
nodeController.dispose(config)
|
||||
main.forceAtLeastOneTab()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package net.corda.demobench.views
|
||||
|
||||
import com.jediterm.terminal.TerminalColor
|
||||
import com.jediterm.terminal.TextStyle
|
||||
import com.jediterm.terminal.ui.settings.DefaultSettingsProvider
|
||||
import java.awt.Dimension
|
||||
import javafx.application.Platform
|
||||
import javafx.embed.swing.SwingNode
|
||||
import javafx.scene.control.Button
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.layout.VBox
|
||||
import javax.swing.SwingUtilities
|
||||
import net.corda.demobench.model.*
|
||||
import net.corda.demobench.pty.R3Pty
|
||||
import net.corda.demobench.rpc.NodeRPC
|
||||
import net.corda.demobench.ui.PropertyLabel
|
||||
import tornadofx.Fragment
|
||||
|
||||
class NodeTerminalView : Fragment() {
|
||||
override val root by fxml<VBox>()
|
||||
|
||||
private val nodeController by inject<NodeController>()
|
||||
private val explorerController by inject<ExplorerController>()
|
||||
private val webServerController by inject<WebServerController>()
|
||||
|
||||
private val nodeName by fxid<Label>()
|
||||
private val p2pPort by fxid<PropertyLabel>()
|
||||
private val states by fxid<PropertyLabel>()
|
||||
private val transactions by fxid<PropertyLabel>()
|
||||
private val balance by fxid<PropertyLabel>()
|
||||
|
||||
private val viewDatabaseButton by fxid<Button>()
|
||||
private val launchWebButton by fxid<Button>()
|
||||
private val launchExplorerButton by fxid<Button>()
|
||||
|
||||
private var isDestroyed: Boolean = false
|
||||
private val explorer = explorerController.explorer()
|
||||
private val webServer = webServerController.webServer()
|
||||
private val viewer = DBViewer()
|
||||
private var rpc: NodeRPC? = null
|
||||
private var pty: R3Pty? = null
|
||||
|
||||
fun open(config: NodeConfig, onExit: () -> Unit) {
|
||||
nodeName.text = config.legalName
|
||||
p2pPort.value = config.artemisPort.toString()
|
||||
launchWebButton.text = "Launch\nWeb Server\n(Port ${config.webPort})"
|
||||
|
||||
val swingTerminal = SwingNode()
|
||||
swingTerminal.setOnMouseClicked {
|
||||
swingTerminal.requestFocus()
|
||||
}
|
||||
|
||||
root.children.add(swingTerminal)
|
||||
root.isVisible = true
|
||||
|
||||
SwingUtilities.invokeLater({
|
||||
val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit)
|
||||
pty = r3pty
|
||||
|
||||
swingTerminal.content = r3pty.terminal
|
||||
nodeController.runCorda(r3pty, config)
|
||||
|
||||
configureDatabaseButton(config)
|
||||
configureExplorerButton(config)
|
||||
configureWebButton(config)
|
||||
|
||||
/*
|
||||
* Start RPC client that will update node statistics on UI.
|
||||
*/
|
||||
rpc = launchRPC(config)
|
||||
})
|
||||
}
|
||||
|
||||
fun enable(config: NodeConfig) {
|
||||
config.state = NodeState.RUNNING
|
||||
log.info("Node '${config.legalName}' is now ready.")
|
||||
|
||||
launchExplorerButton.isDisable = false
|
||||
viewDatabaseButton.isDisable = false
|
||||
launchWebButton.isDisable = false
|
||||
}
|
||||
|
||||
/*
|
||||
* We only want to run one explorer for each node.
|
||||
* So disable the "launch" button when we have
|
||||
* launched the explorer and only reenable it when
|
||||
* the explorer has exited.
|
||||
*/
|
||||
fun configureExplorerButton(config: NodeConfig) {
|
||||
launchExplorerButton.setOnAction {
|
||||
launchExplorerButton.isDisable = true
|
||||
|
||||
explorer.open(config, onExit = {
|
||||
launchExplorerButton.isDisable = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun configureDatabaseButton(config: NodeConfig) {
|
||||
viewDatabaseButton.setOnAction {
|
||||
viewer.openBrowser(config.h2Port)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We only want to run one web server for each node.
|
||||
* So disable the "launch" button when we have
|
||||
* launched the web server and only reenable it when
|
||||
* the web server has exited.
|
||||
*/
|
||||
fun configureWebButton(config: NodeConfig) {
|
||||
launchWebButton.setOnAction {
|
||||
launchWebButton.isDisable = true
|
||||
|
||||
webServer.open(config, onExit = {
|
||||
launchWebButton.isDisable = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun launchRPC(config: NodeConfig) = NodeRPC(config, start = { enable(config) }, invoke = { ops ->
|
||||
try {
|
||||
val verifiedTx = ops.verifiedTransactions()
|
||||
val statesInVault = ops.vaultAndUpdates()
|
||||
val cashBalances = ops.getCashBalances().entries.joinToString(
|
||||
separator = ", ",
|
||||
transform = { e -> e.value.toString() }
|
||||
)
|
||||
|
||||
Platform.runLater {
|
||||
states.value = statesInVault.first.size.toString()
|
||||
transactions.value = verifiedTx.first.size.toString()
|
||||
balance.value = if (cashBalances.isNullOrEmpty()) "0" else cashBalances
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warning("RPC failed: " + e)
|
||||
}
|
||||
})
|
||||
|
||||
fun destroy() {
|
||||
if (!isDestroyed) {
|
||||
webServer.close()
|
||||
explorer.close()
|
||||
viewer.close()
|
||||
rpc?.close()
|
||||
pty?.close()
|
||||
isDestroyed = true
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshTerminal() {
|
||||
// TODO - Force a repaint somehow? My naive attempts have not worked.
|
||||
}
|
||||
|
||||
class TerminalSettingsProvider : DefaultSettingsProvider() {
|
||||
override fun getDefaultStyle() = TextStyle(TerminalColor.WHITE, TerminalColor.BLACK)
|
||||
|
||||
override fun emulateX11CopyPaste() = true
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.h2.server.web
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
class LocalWebServer : WebServer() {
|
||||
|
||||
/**
|
||||
* Create a new session that will not kill the entire
|
||||
* web server if/when we disconnect it.
|
||||
*/
|
||||
override fun addSession(conn: Connection): String {
|
||||
val session = createNewSession("local")
|
||||
session.setConnection(conn)
|
||||
session.put("url", conn.metaData.url)
|
||||
val s = session.get("sessionId") as String
|
||||
return url + "/frame.jsp?jsessionid=" + s
|
||||
}
|
||||
|
||||
}
|
BIN
tools/demobench/src/main/resources/cordalogo.png
Normal file
BIN
tools/demobench/src/main/resources/cordalogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
16
tools/demobench/src/main/resources/logback.xml
Normal file
16
tools/demobench/src/main/resources/logback.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${user.home}/demobench/demobench.log</file>
|
||||
<append>false</append>
|
||||
<encoder>
|
||||
<pattern>%date %-5level %c{1} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
3
tools/demobench/src/main/resources/logging.properties
Normal file
3
tools/demobench/src/main/resources/logging.properties
Normal file
@ -0,0 +1,3 @@
|
||||
# Register SLF4JBridgeHandler as handler for the j.u.l. root logger
|
||||
# See http://www.slf4j.org/legacy.html#jul-to-slf4j
|
||||
handlers = org.slf4j.bridge.SLF4JBridgeHandler
|
@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Pane?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
|
||||
<StackPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0">
|
||||
<tabs>
|
||||
<Tab text="Bank A">
|
||||
<content>
|
||||
<VBox prefHeight="953.0" prefWidth="1363.0">
|
||||
<children>
|
||||
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header">
|
||||
<children>
|
||||
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0">
|
||||
<children>
|
||||
<Label style="-fx-font-size: 40; -fx-text-fill: red;" text="Bank A" />
|
||||
<Label style="-fx-text-fill: white;" text="P2P port: 20004" />
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="93.0" prefWidth="267.0">
|
||||
<children>
|
||||
<Label maxWidth="1.7976931348623157E308" text="States in vault: 20" />
|
||||
<Label text="Known transactions: 45" />
|
||||
<Label text="Balance: $1024" />
|
||||
</children>
|
||||
</VBox>
|
||||
<Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
|
||||
<Button mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="View Database" textAlignment="CENTER" />
|
||||
<Button mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Explorer" textAlignment="CENTER" />
|
||||
</children>
|
||||
</HBox>
|
||||
<StackPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" style="-fx-background-color: black;" VBox.vgrow="ALWAYS">
|
||||
<children>
|
||||
<ImageView fitWidth="1000.0" pickOnBounds="true" preserveRatio="true" StackPane.alignment="TOP_LEFT">
|
||||
<image>
|
||||
<Image url="@mock-term.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
</children>
|
||||
</VBox>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab text="Bank B" />
|
||||
<Tab text="Bank of Corda">
|
||||
<content>
|
||||
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0" />
|
||||
</content>
|
||||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
<Button mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
|
||||
<StackPane.margin>
|
||||
<Insets right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</Button>
|
||||
</children>
|
||||
</StackPane>
|
Binary file not shown.
Before Width: | Height: | Size: 126 KiB |
@ -7,7 +7,7 @@
|
||||
-fx-padding: 15px;
|
||||
}
|
||||
|
||||
.header Label {
|
||||
.property-label .label {
|
||||
-fx-font-size: 14pt;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
@ -20,4 +20,4 @@
|
||||
-fx-base: #009759;
|
||||
-fx-background-radius: 5px;
|
||||
-fx-opacity: 80%;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<MenuBar>
|
||||
<Menu text="File">
|
||||
<MenuItem fx:id="menuOpen" text="Open"/>
|
||||
<MenuItem fx:id="menuSaveAs" disable="true" text="Save As"/>
|
||||
</Menu>
|
||||
</MenuBar>
|
||||
<StackPane VBox.vgrow="ALWAYS">
|
||||
<children>
|
||||
<TabPane fx:id="nodeTabPane" minHeight="444.0" minWidth="800.0" prefHeight="613.0" prefWidth="1231.0" tabClosingPolicy="UNAVAILABLE" tabMinHeight="30.0"/>
|
||||
<Button fx:id="addNodeButton" mnemonicParsing="false" styleClass="add-node-button" text="Add Node" StackPane.alignment="TOP_RIGHT">
|
||||
<StackPane.margin>
|
||||
<Insets right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</Button>
|
||||
</children>
|
||||
</StackPane>
|
||||
</VBox>
|
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Pane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import net.corda.demobench.ui.PropertyLabel?>
|
||||
|
||||
<VBox visible="false" prefHeight="953.0" prefWidth="1363.0" xmlns="http://javafx.com/javafx/8.0.102" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<HBox prefHeight="95.0" prefWidth="800.0" spacing="15.0" styleClass="header">
|
||||
<children>
|
||||
<VBox prefHeight="66.0" prefWidth="296.0" spacing="20.0">
|
||||
<children>
|
||||
<Label fx:id="nodeName" style="-fx-font-size: 40; -fx-text-fill: red;" />
|
||||
<PropertyLabel fx:id="p2pPort" name="P2P port: " />
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="93.0" prefWidth="267.0">
|
||||
<children>
|
||||
<PropertyLabel fx:id="states" name="States in vault: " />
|
||||
<PropertyLabel fx:id="transactions" name="Known transactions: " />
|
||||
<PropertyLabel fx:id="balance" name="Balance: " />
|
||||
</children>
|
||||
</VBox>
|
||||
<Pane prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="viewDatabaseButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="View Database" textAlignment="CENTER" />
|
||||
<Button fx:id="launchWebButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Web Server" textAlignment="CENTER" />
|
||||
<Button fx:id="launchExplorerButton" disable="true" mnemonicParsing="false" prefHeight="92.0" prefWidth="115.0" styleClass="big-button" text="Launch Explorer" textAlignment="CENTER" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
7
tools/demobench/src/main/resources/services.conf
Normal file
7
tools/demobench/src/main/resources/services.conf
Normal file
@ -0,0 +1,7 @@
|
||||
corda.notary.validating
|
||||
corda.notary.simple
|
||||
corda.interest_rates
|
||||
corda.issuer.USD
|
||||
corda.issuer.GBP
|
||||
corda.issuer.CHF
|
||||
corda.cash
|
59
tools/explorer/capsule/build.gradle
Normal file
59
tools/explorer/capsule/build.gradle
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* This build.gradle exists to package Node Explorer as an executable fat jar.
|
||||
*/
|
||||
apply plugin: 'us.kirchmeier.capsule'
|
||||
|
||||
description 'Node Explorer'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
// Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards
|
||||
// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
|
||||
// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
|
||||
// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into
|
||||
// its own module so its target can be controlled individually, but for now this suffices.
|
||||
sourceCompatibility = 1.6
|
||||
targetCompatibility = 1.6
|
||||
|
||||
dependencies {
|
||||
compile project(':tools:explorer')
|
||||
}
|
||||
|
||||
task buildExplorerJAR(type: FatCapsule) {
|
||||
applicationClass 'net.corda.explorer.Main'
|
||||
archiveName "node-explorer-${corda_version}.jar"
|
||||
applicationSource = files(project.tasks.findByName('jar'), '../build/classes/main/ExplorerCaplet.class')
|
||||
classifier 'fat'
|
||||
|
||||
capsuleManifest {
|
||||
applicationVersion = corda_version
|
||||
systemProperties['visualvm.display.name'] = 'Node Explorer'
|
||||
minJavaVersion = '1.8.0'
|
||||
// This version is known to work and avoids earlier 8u versions that have bugs.
|
||||
minUpdateVersion['1.8'] = '102'
|
||||
caplets = ['ExplorerCaplet']
|
||||
|
||||
// JVM configuration:
|
||||
// - Constrain to small heap sizes to ease development on low end devices.
|
||||
// - Switch to the G1 GC which is going to be the default in Java 9 and gives low pause times/string dedup.
|
||||
//
|
||||
// If you change these flags, please also update Driver.kt
|
||||
jvmArgs = ['-Xmx200m', '-XX:+UseG1GC']
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes('Corda-Version': corda_version)
|
||||
}
|
||||
}
|
||||
|
||||
build.dependsOn buildExplorerJAR
|
49
tools/explorer/src/main/java/ExplorerCaplet.java
Normal file
49
tools/explorer/src/main/java/ExplorerCaplet.java
Normal file
@ -0,0 +1,49 @@
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExplorerCaplet extends Capsule {
|
||||
|
||||
protected ExplorerCaplet(Capsule pred) {
|
||||
super(pred);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriding the Caplet classpath generation via the intended interface in Capsule.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> T attribute(Map.Entry<String, T> attr) {
|
||||
// Equality is used here because Capsule never instantiates these attributes but instead reuses the ones
|
||||
// defined as public static final fields on the Capsule class, therefore referential equality is safe.
|
||||
if (ATTR_APP_CLASS_PATH == attr) {
|
||||
T cp = super.attribute(attr);
|
||||
List<Path> classpath = augmentClasspath((List<Path>) cp, "plugins");
|
||||
return (T) augmentClasspath(classpath, "dependencies");
|
||||
}
|
||||
return super.attribute(attr);
|
||||
}
|
||||
|
||||
// TODO: Make directory configurable via the capsule manifest.
|
||||
// TODO: Add working directory variable to capsules string replacement variables.
|
||||
private List<Path> augmentClasspath(List<Path> classpath, String dirName) {
|
||||
File dir = new File(dirName);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
}
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isFile() && isJAR(file)) {
|
||||
classpath.add(file.toPath().toAbsolutePath());
|
||||
}
|
||||
}
|
||||
return classpath;
|
||||
}
|
||||
|
||||
private Boolean isJAR(File file) {
|
||||
return file.getName().toLowerCase().endsWith(".jar");
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,6 @@ import net.corda.client.model.Models
|
||||
import net.corda.client.model.observableValue
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.explorer.model.CordaViewModel
|
||||
@ -28,6 +27,7 @@ import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.node.driver.PortAllocation
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
@ -36,6 +36,7 @@ import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.App
|
||||
import tornadofx.addStageIcon
|
||||
import tornadofx.find
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -57,9 +58,57 @@ class Main : App(MainView::class) {
|
||||
}.showAndWait().get()
|
||||
if (button != ButtonType.OK) it.consume()
|
||||
}
|
||||
stage.hide()
|
||||
loginView.login()
|
||||
stage.show()
|
||||
|
||||
val hostname = parameters.named["host"]
|
||||
val port = asInteger(parameters.named["port"])
|
||||
val username = parameters.named["username"]
|
||||
val password = parameters.named["password"]
|
||||
var isLoggedIn = false
|
||||
|
||||
if ((hostname != null) && (port != null) && (username != null) && (password != null)) {
|
||||
try {
|
||||
// Allow us optionally to override the SSL configuration too.
|
||||
val sslConfig = getSSLConfig()
|
||||
if (sslConfig != null) {
|
||||
loginView.sslConfig = sslConfig
|
||||
}
|
||||
|
||||
loginView.login(hostname, port, username, password)
|
||||
isLoggedIn = true
|
||||
} catch (e: Exception) {
|
||||
ExceptionDialog(e).apply { initOwner(stage.scene.window) }.showAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
stage.hide()
|
||||
loginView.login()
|
||||
stage.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun asInteger(s: String?): Int? {
|
||||
try {
|
||||
return s?.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSSLConfig(): SSLConfiguration? {
|
||||
val certificatesDir = parameters.named["certificatesDir"]
|
||||
val keyStorePassword = parameters.named["keyStorePassword"]
|
||||
val trustStorePassword = parameters.named["trustStorePassword"]
|
||||
|
||||
return if ((certificatesDir != null) && (keyStorePassword != null) && (trustStorePassword != null)) {
|
||||
object: SSLConfiguration {
|
||||
override val certificatesDirectory = Paths.get(certificatesDir)
|
||||
override val keyStorePassword: String = keyStorePassword
|
||||
override val trustStorePassword: String = trustStorePassword
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -38,6 +38,19 @@ class LoginView : View() {
|
||||
private val keyStorePasswordProperty by objectProperty(SettingsModel::keyStorePasswordProperty)
|
||||
private val trustStorePasswordProperty by objectProperty(SettingsModel::trustStorePasswordProperty)
|
||||
|
||||
private var sslConfigValue: SSLConfiguration = object : SSLConfiguration {
|
||||
override val certificatesDirectory: Path get() = certificatesDir.get()
|
||||
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
|
||||
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
|
||||
}
|
||||
var sslConfig : SSLConfiguration
|
||||
get() = sslConfigValue
|
||||
set(value) { sslConfigValue = value }
|
||||
|
||||
fun login(host: String?, port: Int, username: String, password: String) {
|
||||
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(host, port), configureSSL(), username, password)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val status = Dialog<LoginStatus>().apply {
|
||||
dialogPane = root
|
||||
@ -46,7 +59,7 @@ class LoginView : View() {
|
||||
ButtonBar.ButtonData.OK_DONE -> try {
|
||||
root.isDisable = true
|
||||
// TODO : Run this async to avoid UI lockup.
|
||||
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureSSL(), usernameTextField.text, passwordTextField.text)
|
||||
login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text)
|
||||
if (!rememberMe.value) {
|
||||
username.value = ""
|
||||
host.value = ""
|
||||
@ -80,11 +93,6 @@ class LoginView : View() {
|
||||
}
|
||||
|
||||
private fun configureSSL(): SSLConfiguration {
|
||||
val sslConfig = object : SSLConfiguration {
|
||||
override val certificatesDirectory: Path get() = certificatesDir.get()
|
||||
override val keyStorePassword: String get() = keyStorePasswordProperty.get()
|
||||
override val trustStorePassword: String get() = trustStorePasswordProperty.get()
|
||||
}
|
||||
// TODO : Don't use dev certificates.
|
||||
return if (sslConfig.keyStoreFile.exists()) sslConfig else configureTestSSL().apply {
|
||||
alert(Alert.AlertType.WARNING, "", "KeyStore not found in certificates directory.\nDEV certificates will be used by default.")
|
||||
|
Loading…
Reference in New Issue
Block a user