diff --git a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt index 153fb670fd..4b071b2c87 100644 --- a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt +++ b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt @@ -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 diff --git a/settings.gradle b/settings.gradle index 11a8b4a211..c599281ae4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle new file mode 100644 index 0000000000..26e641643c --- /dev/null +++ b/tools/demobench/build.gradle @@ -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) + } + } +} diff --git a/tools/demobench/libs/jediterm-terminal-2.5.jar b/tools/demobench/libs/jediterm-terminal-2.5.jar new file mode 100644 index 0000000000..b211b2d08e Binary files /dev/null and b/tools/demobench/libs/jediterm-terminal-2.5.jar differ diff --git a/tools/demobench/libs/linux/x86/libpty.so b/tools/demobench/libs/linux/x86/libpty.so new file mode 100755 index 0000000000..832504bc8d Binary files /dev/null and b/tools/demobench/libs/linux/x86/libpty.so differ diff --git a/tools/demobench/libs/linux/x86_64/libpty.so b/tools/demobench/libs/linux/x86_64/libpty.so new file mode 100755 index 0000000000..cd41f3b15b Binary files /dev/null and b/tools/demobench/libs/linux/x86_64/libpty.so differ diff --git a/tools/demobench/libs/macosx/x86/libpty.dylib b/tools/demobench/libs/macosx/x86/libpty.dylib new file mode 100755 index 0000000000..f05fb026cf Binary files /dev/null and b/tools/demobench/libs/macosx/x86/libpty.dylib differ diff --git a/tools/demobench/libs/macosx/x86_64/libpty.dylib b/tools/demobench/libs/macosx/x86_64/libpty.dylib new file mode 100755 index 0000000000..1093ba1c84 Binary files /dev/null and b/tools/demobench/libs/macosx/x86_64/libpty.dylib differ diff --git a/tools/demobench/libs/pty4j-0.7.2.jar b/tools/demobench/libs/pty4j-0.7.2.jar new file mode 100644 index 0000000000..e336572696 Binary files /dev/null and b/tools/demobench/libs/pty4j-0.7.2.jar differ diff --git a/tools/demobench/libs/win/x86/libwinpty.dll b/tools/demobench/libs/win/x86/libwinpty.dll new file mode 100644 index 0000000000..e1644b8ee8 Binary files /dev/null and b/tools/demobench/libs/win/x86/libwinpty.dll differ diff --git a/tools/demobench/libs/win/x86/winpty-agent.exe b/tools/demobench/libs/win/x86/winpty-agent.exe new file mode 100644 index 0000000000..3abad80b84 Binary files /dev/null and b/tools/demobench/libs/win/x86/winpty-agent.exe differ diff --git a/tools/demobench/libs/win/x86/winpty.dll b/tools/demobench/libs/win/x86/winpty.dll new file mode 100644 index 0000000000..f07b95a27b Binary files /dev/null and b/tools/demobench/libs/win/x86/winpty.dll differ diff --git a/tools/demobench/libs/win/x86_64/cyglaunch.exe b/tools/demobench/libs/win/x86_64/cyglaunch.exe new file mode 100644 index 0000000000..d84e6e964c Binary files /dev/null and b/tools/demobench/libs/win/x86_64/cyglaunch.exe differ diff --git a/tools/demobench/libs/win/x86_64/winpty-agent.exe b/tools/demobench/libs/win/x86_64/winpty-agent.exe new file mode 100644 index 0000000000..e1963c4e3c Binary files /dev/null and b/tools/demobench/libs/win/x86_64/winpty-agent.exe differ diff --git a/tools/demobench/libs/win/x86_64/winpty.dll b/tools/demobench/libs/win/x86_64/winpty.dll new file mode 100644 index 0000000000..f9bdec0394 Binary files /dev/null and b/tools/demobench/libs/win/x86_64/winpty.dll differ diff --git a/tools/demobench/libs/win/xp/winpty-agent.exe b/tools/demobench/libs/win/xp/winpty-agent.exe new file mode 100644 index 0000000000..cc18efda8c Binary files /dev/null and b/tools/demobench/libs/win/xp/winpty-agent.exe differ diff --git a/tools/demobench/libs/win/xp/winpty.dll b/tools/demobench/libs/win/xp/winpty.dll new file mode 100644 index 0000000000..bd40cd1e9d Binary files /dev/null and b/tools/demobench/libs/win/xp/winpty.dll differ diff --git a/tools/demobench/package-demobench-dmg.sh b/tools/demobench/package-demobench-dmg.sh new file mode 100755 index 0000000000..c72c6d3abe --- /dev/null +++ b/tools/demobench/package-demobench-dmg.sh @@ -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 diff --git a/tools/demobench/package-demobench-exe.bat b/tools/demobench/package-demobench-exe.bat new file mode 100644 index 0000000000..bb6c21191d --- /dev/null +++ b/tools/demobench/package-demobench-exe.bat @@ -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 diff --git a/tools/demobench/package-demobench-rpm.sh b/tools/demobench/package-demobench-rpm.sh new file mode 100755 index 0000000000..3d14661206 --- /dev/null +++ b/tools/demobench/package-demobench-rpm.sh @@ -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 diff --git a/tools/demobench/package/linux/DemoBench.png b/tools/demobench/package/linux/DemoBench.png new file mode 100644 index 0000000000..d72d6ad543 Binary files /dev/null and b/tools/demobench/package/linux/DemoBench.png differ diff --git a/tools/demobench/package/linux/DemoBench.spec b/tools/demobench/package/linux/DemoBench.spec new file mode 100644 index 0000000000..6162007b2e --- /dev/null +++ b/tools/demobench/package/linux/DemoBench.spec @@ -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 diff --git a/tools/demobench/package/macosx/DemoBench-post-image.sh b/tools/demobench/package/macosx/DemoBench-post-image.sh new file mode 100644 index 0000000000..00c80b5ac7 --- /dev/null +++ b/tools/demobench/package/macosx/DemoBench-post-image.sh @@ -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 + diff --git a/tools/demobench/package/macosx/DemoBench-volume.icns b/tools/demobench/package/macosx/DemoBench-volume.icns new file mode 100644 index 0000000000..447d28df81 Binary files /dev/null and b/tools/demobench/package/macosx/DemoBench-volume.icns differ diff --git a/tools/demobench/package/macosx/DemoBench.icns b/tools/demobench/package/macosx/DemoBench.icns new file mode 100644 index 0000000000..447d28df81 Binary files /dev/null and b/tools/demobench/package/macosx/DemoBench.icns differ diff --git a/tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp b/tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp new file mode 100644 index 0000000000..7402e28224 Binary files /dev/null and b/tools/demobench/package/windows/DemoBench-INVALID-setup-icon.bmp differ diff --git a/tools/demobench/package/windows/DemoBench-post-image.wsf b/tools/demobench/package/windows/DemoBench-post-image.wsf new file mode 100644 index 0000000000..b436c06501 --- /dev/null +++ b/tools/demobench/package/windows/DemoBench-post-image.wsf @@ -0,0 +1,24 @@ + + + + + + diff --git a/tools/demobench/package/windows/DemoBench.exe.manifest b/tools/demobench/package/windows/DemoBench.exe.manifest new file mode 100644 index 0000000000..d632713b5e --- /dev/null +++ b/tools/demobench/package/windows/DemoBench.exe.manifest @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + diff --git a/tools/demobench/package/windows/DemoBench.ico b/tools/demobench/package/windows/DemoBench.ico new file mode 100644 index 0000000000..cf9590ceae Binary files /dev/null and b/tools/demobench/package/windows/DemoBench.ico differ diff --git a/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java new file mode 100644 index 0000000000..44e4c4b251 --- /dev/null +++ b/tools/demobench/src/main/java/net/corda/demobench/config/LoggingConfig.java @@ -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 -Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig + * 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; + } + +} diff --git a/tools/demobench/src/main/java/net/corda/demobench/pty/PtyProcessTtyConnector.java b/tools/demobench/src/main/java/net/corda/demobench/pty/PtyProcessTtyConnector.java new file mode 100644 index 0000000000..3648f21480 --- /dev/null +++ b/tools/demobench/src/main/java/net/corda/demobench/pty/PtyProcessTtyConnector.java @@ -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; + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt new file mode 100644 index 0000000000..b9447be9da --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -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) = launch(DemoBench::class.java, *args) + } + + init { + addStageIcon(Image("cordalogo.png")) + } +} + +/* + * Trivial utility function to create SLF4J Logger. + */ +inline fun loggerFor(): Logger = LoggerFactory.getLogger(T::class.java) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt new file mode 100644 index 0000000000..8ff79e1a81 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -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() + } + + 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) + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/ExplorerController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/ExplorerController.kt new file mode 100644 index 0000000000..32813e627b --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/ExplorerController.kt @@ -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() + 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) + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt new file mode 100644 index 0000000000..52a388b7a7 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/HasPlugins.kt @@ -0,0 +1,7 @@ +package net.corda.demobench.model + +import java.nio.file.Path + +interface HasPlugins { + val pluginDir: Path +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt new file mode 100644 index 0000000000..76aed4ae8c --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -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() + private val serviceController by inject() + + @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 { + 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) +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt new file mode 100644 index 0000000000..023f5cd1e7 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt @@ -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 { + return listOf(javaPath.toString(), "-jar", jarPath.toString(), *args) + } + + fun processFor(jarPath: Path, vararg args: String): ProcessBuilder { + return ProcessBuilder(commandFor(jarPath, *args)) + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt new file mode 100644 index 0000000000..cdfdf77522 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt @@ -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() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt new file mode 100644 index 0000000000..2a63f9ae0a --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -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, + val users: List = 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) { + if (plugins.isNotEmpty() && pluginDir.toFile().forceDirectory()) { + plugins.forEach { + Files.copy(it, pluginDir.resolve(it.fileName.toString()), StandardCopyOption.REPLACE_EXISTING) + } + } + } + +} + +private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) + +private fun addressValueFor(port: Int) = valueFor("localhost:$port") + +private inline fun 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() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt new file mode 100644 index 0000000000..de33d473e4 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -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() + private val pluginController by inject() + + 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() + private val port = AtomicInteger(firstPort) + + private var networkMapConfig: NetworkMapConfig? = null + + val activeNodes: List 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)) + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt new file mode 100644 index 0000000000..c69e9c54fa --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -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().observable()) + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt new file mode 100644 index 0000000000..d6b5165b5f --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt @@ -0,0 +1,14 @@ +package net.corda.demobench.model + +import tornadofx.ItemViewModel + +class NodeDataModel : ItemViewModel(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 } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeState.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeState.kt new file mode 100644 index 0000000000..3147fa7d27 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeState.kt @@ -0,0 +1,7 @@ +package net.corda.demobench.model + +enum class NodeState { + STARTING, + RUNNING, + DEAD +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt new file mode 100644 index 0000000000..62c4ee76ed --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt @@ -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 = loadConf(resources.url(resourceName)) + + val notaries: List = services.filter { it.startsWith("corda.notary.") }.toList() + + /* + * Load our list of known extra Corda services. + */ + private fun loadConf(url: URL?): List { + if (url == null) { + return emptyList() + } else { + try { + val set = TreeSet() + 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() + } + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt new file mode 100644 index 0000000000..4d16f6dd9b --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt @@ -0,0 +1,18 @@ +package net.corda.demobench.model + +data class User(val user: String, val password: String, val permissions: List) { + fun toMap() = mapOf( + "user" to user, + "password" to password, + "permissions" to permissions + ) +} + +@Suppress("UNCHECKED_CAST") +fun toUser(map: Map) = User( + map.getOrElse("user", { "none" }) as String, + map.getOrElse("password", { "none" }) as String, + map.getOrElse("permissions", { emptyList() }) as List +) + +fun user(name: String) = User(name, "letmein", listOf("ALL")) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt new file mode 100644 index 0000000000..6c69585f5a --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/PluginController.kt @@ -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() + 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 = walkPlugins(config.pluginDir) + .filter { bankOfCorda.name != it.fileName.toString() } + + private fun walkPlugins(pluginDir: Path): Stream { + 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/") diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt new file mode 100644 index 0000000000..0d32a2b396 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -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() + private val baseDir: Path = jvm.dataHome + private val nodeController by inject() + private val pluginController by inject() + private val installFactory by inject() + 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? { + val chosen = chooser.showOpenDialog(null) ?: return null + log.info("Selected profile: $chosen") + + val configs = LinkedList() + + 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) + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt new file mode 100644 index 0000000000..4dfa11579d --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -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() + } + + 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, environment: Map, 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, envs: Map, workingDir: String?) { + check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" }) + + val environment = HashMap(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() + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt new file mode 100644 index 0000000000..6ea1355fe9 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -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() + 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() + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/CloseableTab.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/CloseableTab.kt new file mode 100644 index 0000000000..95c83f9a92 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/CloseableTab.kt @@ -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: JDK-8091261 + */ +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) + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt new file mode 100644 index 0000000000..25cfb953ac --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/PropertyLabel.kt @@ -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") + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt new file mode 100644 index 0000000000..14d5970dff --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt @@ -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() + + private val profileController by inject() + private val nodeController by inject() + private val addNodeButton by fxid + + diff --git a/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml new file mode 100644 index 0000000000..ac628de248 --- /dev/null +++ b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + +