From 48295242444cba75f4f1014fed6093c4b8d22812 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 4 Apr 2017 17:25:37 +0100 Subject: [PATCH] Avoid errors if Corda / Explorer cannot be found at runtime. (#501) * Avoid NPE if corda.jar cannot be found. * Log an error if Node Explorer or WebServer fail to start. * Display an error dialog if we cannot find corda.jar at start-up. * Use official notUsed() function for unwanted Observables. * Fix unit tests. * Rename function to readErrorLines(). --- .../kotlin/net/corda/demobench/DemoBench.kt | 4 +++ .../net/corda/demobench/explorer/Explorer.kt | 10 ++++-- .../net/corda/demobench/model/JVMConfig.kt | 21 ++++++++++++ .../corda/demobench/model/NodeController.kt | 6 +++- .../kotlin/net/corda/demobench/pty/R3Pty.kt | 5 +-- .../corda/demobench/views/NodeTerminalView.kt | 34 +++++++++++++------ .../net/corda/demobench/web/WebServer.kt | 10 ++++-- .../demobench/model/NodeControllerTest.kt | 2 +- 8 files changed, 73 insertions(+), 19 deletions(-) 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 02e8983dce..ebe2141987 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/DemoBench.kt @@ -4,6 +4,8 @@ import javafx.scene.image.Image import net.corda.demobench.views.DemoBenchView import tornadofx.App import tornadofx.addStageIcon +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets.UTF_8 /** * README! @@ -49,3 +51,5 @@ class DemoBench : App(DemoBenchView::class) { addStageIcon(Image("cordalogo.png")) } } + +fun Process.readErrorLines(): List = InputStreamReader(this.errorStream, UTF_8).readLines() 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 index 7a9309ce71..3deab00a0d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -1,6 +1,7 @@ package net.corda.demobench.explorer import net.corda.core.utilities.loggerFor +import net.corda.demobench.readErrorLines import java.io.IOException import java.util.concurrent.Executors import net.corda.demobench.model.NodeConfig @@ -40,13 +41,18 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Close these streams because no-one is using them. safeClose(p.outputStream) safeClose(p.inputStream) - safeClose(p.errorStream) executor.submit { val exitValue = p.waitFor() + val errors = p.readErrorLines() process = null - log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue) + if (errors.isEmpty()) { + log.info("Node Explorer for '{}' has exited (value={})", config.legalName, exitValue) + } else { + log.error("Node Explorer for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + } + onExit(config) } } catch (e: IOException) { 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 index 023f5cd1e7..5be963a0a9 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt @@ -2,6 +2,9 @@ package net.corda.demobench.model import java.nio.file.Path import java.nio.file.Paths +import javafx.scene.control.Alert +import javafx.scene.control.Alert.AlertType.ERROR +import javafx.stage.Stage import tornadofx.Controller class JVMConfig : Controller() { @@ -24,3 +27,21 @@ class JVMConfig : Controller() { } } + +typealias atRuntime = (Path, String) -> Unit + +fun checkExists(path: Path, header: String) { + if (!path.toFile().exists()) { + val alert = Alert(ERROR) + alert.isResizable = true + alert.headerText = header + alert.contentText = "'$path' does not exist.\n" + + "Please install all of DemoBench's runtime dependencies or run the installer. " + + "See the documentation for more details." + + val stage = alert.dialogPane.scene.window as Stage + stage.isAlwaysOnTop = true + + alert.show() + } +} 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 index de33d473e4..4d869cbe31 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -13,7 +13,7 @@ import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty import tornadofx.Controller -class NodeController : Controller() { +class NodeController(check: atRuntime = ::checkExists) : Controller() { companion object { const val firstPort = 10000 const val minPort = 1024 @@ -39,6 +39,10 @@ class NodeController : Controller() { init { log.info("Base directory: $baseDir") log.info("Corda JAR: $cordaPath") + + // Check that the Corda capsule is available. + // We do NOT want to do this during unit testing! + check(cordaPath, "Cannot find Corda JAR.") } /** 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 index 9fdd06acf8..6e6e9efbb8 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -1,6 +1,5 @@ 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 @@ -21,13 +20,15 @@ class R3Pty(val name: String, settings: SettingsProvider, dimension: Dimension, val terminal = JediTermWidget(dimension, settings) + val isConnected: Boolean get() = terminal.ttyConnector?.isConnected ?: false + override fun close() { log.info("Closing terminal '{}'", name) executor.shutdown() terminal.close() } - private fun createTtyConnector(command: Array, environment: Map, workingDir: String?): TtyConnector { + private fun createTtyConnector(command: Array, environment: Map, workingDir: String?): PtyProcessTtyConnector { val process = PtyProcess.exec(command, environment, workingDir) try { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index eb09805b25..eea4a4e23a 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -5,12 +5,13 @@ import com.jediterm.terminal.TextStyle import com.jediterm.terminal.ui.settings.DefaultSettingsProvider import java.awt.Dimension import java.util.logging.Level +import javax.swing.SwingUtilities import javafx.application.Platform import javafx.embed.swing.SwingNode import javafx.scene.control.Button import javafx.scene.control.Label import javafx.scene.layout.VBox -import javax.swing.SwingUtilities +import net.corda.client.rpc.notUsed import net.corda.demobench.explorer.ExplorerController import net.corda.demobench.model.* import net.corda.demobench.pty.R3Pty @@ -61,17 +62,28 @@ class NodeTerminalView : Fragment() { val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit) pty = r3pty - swingTerminal.content = r3pty.terminal - nodeController.runCorda(r3pty, config) + if (nodeController.runCorda(r3pty, config)) { + swingTerminal.content = r3pty.terminal - configureDatabaseButton(config) - configureExplorerButton(config) - configureWebButton(config) + configureDatabaseButton(config) + configureExplorerButton(config) + configureWebButton(config) - /* - * Start RPC client that will update node statistics on UI. - */ - rpc = launchRPC(config) + /* + * Start RPC client that will update node statistics on UI. + */ + rpc = launchRPC(config) + + /* + * Check whether the PTY has exited unexpectedly, + * and close the RPC client if it has. + */ + if (!r3pty.isConnected) { + log.severe("Node '${config.legalName}' has failed to start.") + swingTerminal.content = null + rpc?.close() + } + } }) } @@ -158,7 +170,7 @@ class NodeTerminalView : Fragment() { // TODO - Will change when we modify RPC Observables handling. private fun fetchAndDrop(pair: Pair>): T { - pair.second.subscribe().unsubscribe() + pair.second.notUsed() return pair.first } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt index 27d96113ac..279b10fcdd 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/web/WebServer.kt @@ -1,6 +1,7 @@ package net.corda.demobench.web import net.corda.core.utilities.loggerFor +import net.corda.demobench.readErrorLines import java.io.IOException import java.util.concurrent.Executors import net.corda.demobench.model.NodeConfig @@ -34,13 +35,18 @@ class WebServer internal constructor(private val webServerController: WebServerC // Close these streams because no-one is using them. safeClose(p.outputStream) safeClose(p.inputStream) - safeClose(p.errorStream) executor.submit { val exitValue = p.waitFor() + val errors = p.readErrorLines() process = null - log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue) + if (errors.isEmpty()) { + log.info("Web Server for '{}' has exited (value={})", config.legalName, exitValue) + } else { + log.error("Web Server for '{}' has exited (value={}, {})", config.legalName, exitValue, errors) + } + onExit(config) } } catch (e: IOException) { diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index 53051097cd..4593313b2f 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -9,7 +9,7 @@ import org.junit.Test class NodeControllerTest { private val baseDir: Path = Paths.get(".").toAbsolutePath() - private val controller = NodeController() + private val controller = NodeController({_,_ ->}) @Test fun `test unique nodes after validate`() {