diff --git a/build.gradle b/build.gradle index 78c6cd1a9d..00017526b2 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/doorman/src/main/resources/reference.conf b/doorman/src/main/resources/reference.conf index 355d6f1095..dcf1ccff1c 100644 --- a/doorman/src/main/resources/reference.conf +++ b/doorman/src/main/resources/reference.conf @@ -11,3 +11,11 @@ dataSourceProperties { "dataSource.password" = "" } h2port = 0 + +jiraConfig{ + address = "https://doorman-jira-host/" + projectCode = "TD" + username = "username" + password = "password" + doneTransitionCode = 41 +} diff --git a/node/build.gradle b/node/build.gradle index a061ad7f4f..0b79c39e63 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -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) diff --git a/package-demobench-dmg.sh b/package-demobench-dmg.sh new file mode 100755 index 0000000000..18f4c6c1ba --- /dev/null +++ b/package-demobench-dmg.sh @@ -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 $* diff --git a/package-demobench-exe.bat b/package-demobench-exe.bat new file mode 100644 index 0000000000..682fd3de1f --- /dev/null +++ b/package-demobench-exe.bat @@ -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 diff --git a/settings.gradle b/settings.gradle index 966b143624..3a82a6f1b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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' diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index c6dded517b..8977461c1b 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -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) + } + } +} + 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/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..48b2f3c9f1 --- /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}/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 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..14c2c28bea --- /dev/null +++ b/tools/demobench/package/macosx/DemoBench-post-image.sh @@ -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 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..ebb02900db --- /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 index 84b84da37a..b9447be9da 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -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) = launch(DemoBench::class.java, *args) } -} \ No newline at end of file + + 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/DemoBenchView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBenchView.kt deleted file mode 100644 index e81c5f706e..0000000000 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBenchView.kt +++ /dev/null @@ -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") - } -} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt new file mode 100644 index 0000000000..6047cd5ec0 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/DBViewer.kt @@ -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() + } + + 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) + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt new file mode 100644 index 0000000000..8ba46f4e06 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/Explorer.kt @@ -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() + } + + 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) + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ExplorerController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ExplorerController.kt new file mode 100644 index 0000000000..b0325be4b8 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ExplorerController.kt @@ -0,0 +1,18 @@ +package net.corda.demobench.model + +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/JVMConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt new file mode 100644 index 0000000000..4b2eb284db --- /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 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 { + return arrayOf(javaPath.toString(), "-jar", jarPath.toString(), *args) + } + + fun processFor(jarPath: Path, vararg args: String): ProcessBuilder { + return ProcessBuilder(commandFor(jarPath, *args).toList()) + } + +} + 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..326b9fd60c --- /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 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() 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..5a3b5b8654 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -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, + val users: List> = listOf(defaultUser), + var networkMap: NetworkMapConfig? = null +) : NetworkMapConfig(legalName, artemisPort) { + + companion object { + val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + + val defaultUser: Map = 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 valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) + +private fun addressValueFor(port: Int) = valueFor("localhost:$port") + +private 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) +} 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..e61ec57707 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -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() + + 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() + 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") + } + + 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)) + +} 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..921d311882 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -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().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..2bde3ce0ea --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeDataModel.kt @@ -0,0 +1,13 @@ +package net.corda.demobench.model + +import tornadofx.ItemViewModel + +class NodeDataModel : ItemViewModel(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 } + +} 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..0094217d54 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt @@ -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 = loadConf(javaClass.classLoader.getResource("services.conf")) + + 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 { + 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() + } + } + +} diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt new file mode 100644 index 0000000000..2e38b9517e --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServer.kt @@ -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() + } + + 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) + } + } + +} \ No newline at end of file diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServerController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServerController.kt new file mode 100644 index 0000000000..60718e74d4 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/WebServerController.kt @@ -0,0 +1,18 @@ +package net.corda.demobench.model + +import tornadofx.Controller + +class WebServerController : Controller() { + + private val jvm by inject() + 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) + +} \ No newline at end of file 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..a820fae9bf --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -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() + private val baseDir = jvm.userHome.resolve("demobench") + private val nodeController by inject() + private val serviceController by inject() + 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? { + val chosen = chooser.showOpenDialog(null) ?: return null + log.info("Selected profile: $chosen") + + val configs = LinkedList() + + 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 { + 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() + } +} 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..d817b022c0 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -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() + } + + 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 + } + } + + 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..23dd0a0671 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -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() + 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() + } + +} 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..27b3c8c743 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/ui/CloseableTab.kt @@ -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 + +} 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..cef277c1b8 --- /dev/null +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/DemoBenchView.kt @@ -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() + + 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/mock-term.png b/tools/demobench/src/main/resources/net/corda/demobench/mock-term.png deleted file mode 100644 index 4df6fc8808..0000000000 Binary files a/tools/demobench/src/main/resources/net/corda/demobench/mock-term.png and /dev/null differ diff --git a/tools/demobench/src/main/resources/net/corda/demobench/style.css b/tools/demobench/src/main/resources/net/corda/demobench/style.css index e25cf329c2..d5453f6a36 100644 --- a/tools/demobench/src/main/resources/net/corda/demobench/style.css +++ b/tools/demobench/src/main/resources/net/corda/demobench/style.css @@ -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%; -} \ No newline at end of file +} diff --git a/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml b/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml new file mode 100644 index 0000000000..4ec4a4ecaa --- /dev/null +++ b/tools/demobench/src/main/resources/net/corda/demobench/views/DemoBenchView.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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..0b2e85587d --- /dev/null +++ b/tools/demobench/src/main/resources/net/corda/demobench/views/NodeTerminalView.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +