diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index ec66c85988..26d9bf09d5 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -40,7 +40,7 @@ data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val mess /** * This model exposes raw event streams to and from the node. */ -class NodeMonitorModel { +class NodeMonitorModel : AutoCloseable { private val retryableStateMachineUpdatesSubject = PublishSubject.create() private val stateMachineUpdatesSubject = PublishSubject.create() @@ -49,6 +49,7 @@ class NodeMonitorModel { private val stateMachineTransactionMappingSubject = PublishSubject.create() private val progressTrackingSubject = PublishSubject.create() private val networkMapSubject = PublishSubject.create() + private var rpcConnection: CordaRPCConnection? = null val stateMachineUpdates: Observable = stateMachineUpdatesSubject val vaultUpdates: Observable> = vaultUpdatesSubject @@ -84,6 +85,17 @@ class NodeMonitorModel { */ class CordaRPCOpsWrapper(val cordaRPCOps: CordaRPCOps) + /** + * Disconnects from the Corda node for a clean client shutdown. + */ + override fun close() { + try { + rpcConnection?.notifyServerAndClose() + } catch (e: Exception) { + logger.error("Error closing RPC connection to node", e) + } + } + /** * Register for updates to/from a given vault. * TODO provide an unsubscribe mechanism @@ -145,9 +157,10 @@ class NodeMonitorModel { } private fun performRpcReconnect(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, shouldRetry: Boolean): List { - - val connection = establishConnectionWithRetry(nodeHostAndPort, username, password, shouldRetry) - val proxy = connection.proxy + val proxy = establishConnectionWithRetry(nodeHostAndPort, username, password, shouldRetry).let { connection -> + rpcConnection = connection + connection.proxy + } val (stateMachineInfos, stateMachineUpdatesRaw) = proxy.stateMachinesFeed() @@ -162,7 +175,7 @@ class NodeMonitorModel { // It is good idea to close connection to properly mark the end of it. During re-connect we will create a new // client and a new connection, so no going back to this one. Also the server might be down, so we are // force closing the connection to avoid propagation of notification to the server side. - connection.forceClose() + rpcConnection?.forceClose() // Perform re-connect. performRpcReconnect(nodeHostAndPort, username, password, shouldRetry = true) }) @@ -175,18 +188,17 @@ class NodeMonitorModel { } private fun establishConnectionWithRetry(nodeHostAndPort: NetworkHostAndPort, username: String, password: String, shouldRetry: Boolean): CordaRPCConnection { - val retryInterval = 5.seconds + val client = CordaRPCClient( + nodeHostAndPort, + CordaRPCClientConfiguration.DEFAULT.copy( + connectionMaxRetryInterval = retryInterval + ) + ) do { val connection = try { logger.info("Connecting to: $nodeHostAndPort") - val client = CordaRPCClient( - nodeHostAndPort, - CordaRPCClientConfiguration.DEFAULT.copy( - connectionMaxRetryInterval = retryInterval - ) - ) val _connection = client.start(username, password) // Check connection is truly operational before returning it. val nodeInfo = _connection.proxy.nodeInfo() @@ -195,7 +207,7 @@ class NodeMonitorModel { } catch (throwable: Throwable) { if (shouldRetry) { // Deliberately not logging full stack trace as it will be full of internal stacktraces. - logger.info("Exception upon establishing connection: " + throwable.message) + logger.info("Exception upon establishing connection: {}", throwable.message) null } else { throw throwable 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 9b80d26add..98503fa1d4 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt @@ -10,6 +10,7 @@ import javafx.stage.Stage import jfxtras.resources.JFXtrasFontRoboto import joptsimple.OptionParser import net.corda.client.jfx.model.Models +import net.corda.client.jfx.model.NodeMonitorModel import net.corda.client.jfx.model.observableValue import net.corda.core.utilities.contextLogger import net.corda.explorer.model.CordaViewModel @@ -21,6 +22,7 @@ import org.controlsfx.dialog.ExceptionDialog import tornadofx.App import tornadofx.addStageIcon import tornadofx.find +import kotlin.system.exitProcess /** * Main class for Explorer, you will need Tornado FX to run the explorer. @@ -34,6 +36,8 @@ class Main : App(MainView::class) { } override fun start(stage: Stage) { + var nodeModel: NodeMonitorModel? = null + // Login to Corda node super.start(stage) stage.minHeight = 600.0 @@ -43,27 +47,29 @@ class Main : App(MainView::class) { val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply { initOwner(stage.scene.window) }.showAndWait().get() - if (button != ButtonType.OK) it.consume() + if (button == ButtonType.OK) { + nodeModel?.close() + } else { + it.consume() + } } 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 + nodeModel = loginView.login(hostname, port, username, password) } catch (e: Exception) { ExceptionDialog(e).apply { initOwner(stage.scene.window) }.showAndWait() } } - if (!isLoggedIn) { + if (nodeModel == null) { stage.hide() - loginView.login() + nodeModel = loginView.login() stage.show() } } @@ -84,7 +90,7 @@ class Main : App(MainView::class) { runInFxApplicationThread { // [showAndWait] need to be in the FX thread. ExceptionDialog(throwable).showAndWait() - System.exit(1) + exitProcess(1) } } // Do this first before creating the notification bar, so it can autosize itself properly. 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 302f0253dd..440d1f0c08 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 @@ -27,11 +27,14 @@ class LoginView : View(WINDOW_TITLE) { private val port by objectProperty(SettingsModel::portProperty) private val fullscreen by objectProperty(SettingsModel::fullscreenProperty) - fun login(host: String, port: Int, username: String, password: String) { - getModel().register(NetworkHostAndPort(host, port), username, password) + fun login(host: String, port: Int, username: String, password: String): NodeMonitorModel { + return getModel().apply { + register(NetworkHostAndPort(host, port), username, password) + } } - fun login() { + tailrec fun login(): NodeMonitorModel? { + var nodeModel: NodeMonitorModel? = null val status = Dialog().apply { dialogPane = root setResultConverter { @@ -39,7 +42,7 @@ class LoginView : View(WINDOW_TITLE) { ButtonBar.ButtonData.OK_DONE -> try { root.isDisable = true // TODO : Run this async to avoid UI lockup. - login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text) + nodeModel = login(hostTextField.text, portProperty.value, usernameTextField.text, passwordTextField.text) if (!rememberMe.value) { username.value = "" host.value = "" @@ -64,12 +67,13 @@ class LoginView : View(WINDOW_TITLE) { initOwner(root.scene.window) }.showAndWait().get() if (button == ButtonType.OK) { + nodeModel?.close() exitProcess(0) } } } }.showAndWait().get() - if (status != LoginStatus.loggedIn) login() + return if (status == LoginStatus.loggedIn) nodeModel else login() } init {