From f9ac03287c271a3394474a8e4d218ce507bf51f6 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 31 Jan 2017 17:54:26 +0000 Subject: [PATCH] Create a capsule for Node Explorer, and allow login via command line parameters to bypass login screen. --- settings.gradle | 1 + tools/explorer/capsule/build.gradle | 54 +++++++++++++++++++ .../src/main/java/ExplorerCaplet.java | 49 +++++++++++++++++ .../main/kotlin/net/corda/explorer/Main.kt | 36 +++++++++++-- .../net/corda/explorer/views/LoginView.kt | 6 ++- 5 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 tools/explorer/capsule/build.gradle create mode 100644 tools/explorer/src/main/java/ExplorerCaplet.java diff --git a/settings.gradle b/settings.gradle index 5848cc6751..bfcc9ae59e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include 'experimental:sandbox' include 'test-utils' include 'tools:demobench' include 'tools:explorer' +include 'tools:explorer:capsule' 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/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle new file mode 100644 index 0000000000..2eb888a3c9 --- /dev/null +++ b/tools/explorer/capsule/build.gradle @@ -0,0 +1,54 @@ +/** + * This build.gradle exists to package Node Explorer as an executable fat jar. + */ +apply plugin: 'us.kirchmeier.capsule' + +description 'Node Explorer' + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + } + jcenter() + maven { + url 'https://dl.bintray.com/kotlin/exposed' + } +} + +// Force the Caplet to target Java 6. This ensures that running 'java -jar explorer.jar' on any Java 6 VM upwards +// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get +// a sensible error message telling you what to do rather than a bytecode version exception that doesn't. +// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into +// its own module so its target can be controlled individually, but for now this suffices. +sourceCompatibility = 1.6 +targetCompatibility = 1.6 + +dependencies { + compile project(':tools:explorer') +} + +task buildExplorerJAR(type: FatCapsule) { + applicationClass 'net.corda.explorer.Main' + archiveName "node-explorer-${corda_version}.jar" + applicationSource = files(project.tasks.findByName('jar'), '../build/classes/main/ExplorerCaplet.class') + classifier 'fat' + + capsuleManifest { + systemProperties['visualvm.display.name'] = 'Node Explorer' + minJavaVersion = '1.8.0' + // This version is known to work and avoids earlier 8u versions that have bugs. + minUpdateVersion['1.8'] = '102' + caplets = ['ExplorerCaplet'] + + // JVM configuration: + // - Constrain to small heap sizes to ease development on low end devices. + // - Switch to the G1 GC which is going to be the default in Java 9 and gives low pause times/string dedup. + // + // If you change these flags, please also update Driver.kt + jvmArgs = ['-Xmx200m', '-XX:+UseG1GC'] + } +} + +build.dependsOn buildExplorerJAR diff --git a/tools/explorer/src/main/java/ExplorerCaplet.java b/tools/explorer/src/main/java/ExplorerCaplet.java new file mode 100644 index 0000000000..e09ac30364 --- /dev/null +++ b/tools/explorer/src/main/java/ExplorerCaplet.java @@ -0,0 +1,49 @@ +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +public class ExplorerCaplet extends Capsule { + + protected ExplorerCaplet(Capsule pred) { + super(pred); + } + + /** + * Overriding the Caplet classpath generation via the intended interface in Capsule. + */ + @Override + @SuppressWarnings("unchecked") + protected T attribute(Map.Entry attr) { + // Equality is used here because Capsule never instantiates these attributes but instead reuses the ones + // defined as public static final fields on the Capsule class, therefore referential equality is safe. + if (ATTR_APP_CLASS_PATH == attr) { + T cp = super.attribute(attr); + List classpath = augmentClasspath((List) cp, "plugins"); + return (T) augmentClasspath(classpath, "dependencies"); + } + return super.attribute(attr); + } + + // TODO: Make directory configurable via the capsule manifest. + // TODO: Add working directory variable to capsules string replacement variables. + private List augmentClasspath(List classpath, String dirName) { + File dir = new File(dirName); + if (!dir.exists()) { + dir.mkdir(); + } + + File[] files = dir.listFiles(); + for (File file : files) { + if (file.isFile() && isJAR(file)) { + classpath.add(file.toPath().toAbsolutePath()); + } + } + return classpath; + } + + private Boolean isJAR(File file) { + return file.getName().toLowerCase().endsWith(".jar"); + } + +} diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt index 130b09b933..93daf83e88 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt @@ -55,9 +55,39 @@ class Main : App(MainView::class) { }.showAndWait().get() if (button != ButtonType.OK) it.consume() } - stage.hide() - loginView.login() - stage.show() + + val hostname = parameters.named["host"] + val port = asInteger(parameters.named["port"]) + val username = parameters.named["username"] + val password = parameters.named["password"] + var isLoggedIn = false + + if ((hostname != null) && (port != null) && (username != null) && (password != null)) { + try { + loginView.login(hostname, port, username, password) + isLoggedIn = true + } catch (e: Exception) { + ExceptionDialog(e).apply { initOwner(stage.scene.window) }.showAndWait() + } + } + + if (!isLoggedIn) { + stage.hide() + loginView.login() + stage.show() + } + } + + private fun asInteger(s: String?): Int? { + if (s == null) { + return null; + } + + try { + return s.toInt(); + } catch (e: NumberFormatException) { + return null + } } init { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt index 4ba675da68..ec74a96e1b 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/LoginView.kt @@ -38,6 +38,10 @@ class LoginView : View() { private val keyStorePasswordProperty by objectProperty(SettingsModel::keyStorePasswordProperty) private val trustStorePasswordProperty by objectProperty(SettingsModel::trustStorePasswordProperty) + fun login(host: String?, port: Int, username: String, password: String) { + getModel().register(HostAndPort.fromParts(host, port), configureSSL(), username, password) + } + fun login() { val status = Dialog().apply { dialogPane = root @@ -46,7 +50,7 @@ class LoginView : View() { ButtonBar.ButtonData.OK_DONE -> try { root.isDisable = true // TODO : Run this async to avoid UI lockup. - getModel().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureSSL(), usernameTextField.text, passwordTextField.text) + login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text) if (!rememberMe.value) { username.value = "" host.value = ""