CORPRIV-665: Ensure tab closes if the node exits.

This commit is contained in:
Chris Rankin 2017-02-09 15:01:29 +00:00
parent 6ae8a4da83
commit b29235e7cd
12 changed files with 136 additions and 114 deletions

View File

@ -1,80 +0,0 @@
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static java.nio.charset.StandardCharsets.UTF_8;
public class R3Pty implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(R3Pty.class);
private final JediTermWidget terminal;
private final String name;
public R3Pty(String name, SettingsProvider settings, Dimension dimension) {
terminal = new JediTermWidget(dimension, settings);
this.name = name;
}
@Override
public void close() {
LOG.info("Closing terminal '{}'", name);
terminal.close();
}
public String getName() {
return name;
}
public JediTermWidget getTerminal() {
return terminal;
}
private TtyConnector createTtyConnector(String[] command, Map<String, String> environment, String workingDir) {
try {
PtyProcess process = PtyProcess.exec(command, environment, workingDir);
try {
return new PtyProcessTtyConnector(name, process, UTF_8);
} catch (Exception e) {
process.destroyForcibly();
process.waitFor(30, TimeUnit.SECONDS);
throw e;
}
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
public void run(String[] args, Map<String, String> envs, String workingDir) {
if (terminal.isSessionRunning()) {
throw new IllegalStateException(terminal.getSessionName() + " is already running");
}
Map<String, String> environment = new HashMap<>(envs);
if (!UIUtil.isWindows) {
environment.put("TERM", "xterm");
}
TerminalSession session = terminal.createTerminalSession(createTtyConnector(args, environment, workingDir));
session.start();
}
public void run(String[] args, Map<String, String> envs) {
run(args, envs, null);
}
public void run(String... args) {
run(args, System.getenv());
}
}

View File

@ -10,7 +10,7 @@ class ExplorerController : Controller() {
private val explorerPath = jvm.applicationDir.resolve("explorer").resolve("node-explorer.jar") private val explorerPath = jvm.applicationDir.resolve("explorer").resolve("node-explorer.jar")
init { init {
log.info("Explorer JAR: " + explorerPath) log.info("Explorer JAR: $explorerPath")
} }
internal fun execute(cwd: Path, vararg args: String) = jvm.execute(explorerPath, cwd, *args) internal fun execute(cwd: Path, vararg args: String) = jvm.execute(explorerPath, cwd, *args)

View File

@ -8,10 +8,10 @@ class JVMConfig : Controller() {
val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath() val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath()
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val applicationDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath() val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath()
init { init {
log.info("Java executable: " + javaPath) log.info("Java executable: $javaPath")
} }
fun commandFor(jarPath: Path, vararg args: String): Array<String> { fun commandFor(jarPath: Path, vararg args: String): Array<String> {

View File

@ -38,6 +38,8 @@ class NodeConfig(
var networkMap: NetworkMapConfig? = null var networkMap: NetworkMapConfig? = null
var state: NodeState = NodeState.STARTING
/* /*
* The configuration object depends upon the networkMap, * The configuration object depends upon the networkMap,
* which is mutable. * which is mutable.

View File

@ -38,8 +38,8 @@ class NodeController : Controller() {
private var networkMapConfig: NetworkMapConfig? = null private var networkMapConfig: NetworkMapConfig? = null
init { init {
log.info("Base directory: " + baseDir) log.info("Base directory: $baseDir")
log.info("Corda JAR: " + cordaPath) log.info("Corda JAR: $cordaPath")
} }
fun validate(nodeData: NodeData): NodeConfig? { fun validate(nodeData: NodeData): NodeConfig? {
@ -54,7 +54,7 @@ class NodeController : Controller() {
) )
if (nodes.putIfAbsent(config.key, config) != null) { if (nodes.putIfAbsent(config.key, config) != null) {
log.warning("Node with key '" + config.key + "' already exists.") log.warning("Node with key '${config.key}' already exists.")
return null return null
} }
@ -64,6 +64,14 @@ class NodeController : Controller() {
return 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 val nextPort: Int get() = port.andIncrement
fun isPortAvailable(port: Int): Boolean { fun isPortAvailable(port: Int): Boolean {
@ -90,7 +98,7 @@ class NodeController : Controller() {
config.networkMap = networkMapConfig config.networkMap = networkMapConfig
} else { } else {
networkMapConfig = config networkMapConfig = config
log.info("Network map provided by: " + config.legalName) log.info("Network map provided by: ${config.legalName}")
} }
} }
@ -112,7 +120,7 @@ class NodeController : Controller() {
// Execute the Corda node // Execute the Corda node
pty.run(command, System.getenv(), nodeDir.toString()) pty.run(command, System.getenv(), nodeDir.toString())
log.info("Launched node: " + config.legalName) log.info("Launched node: ${config.legalName}")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
log.severe("Failed to launch Corda:" + e) log.severe("Failed to launch Corda:" + e)

View File

@ -0,0 +1,7 @@
package net.corda.demobench.model
enum class NodeState {
STARTING,
RUNNING,
DEAD
}

View File

@ -24,7 +24,7 @@ class ServiceController : Controller() {
val service = it.trim() val service = it.trim()
set.add(service) set.add(service)
log.info("Supports: " + service) log.info("Supports: $service")
} }
} }
return set.toList() return set.toList()

View File

@ -0,0 +1,66 @@
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 org.slf4j.LoggerFactory
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 val log = LoggerFactory.getLogger(R3Pty::class.java)
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<String>, environment: Map<String, String>, workingDir: String?): TtyConnector {
try {
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
}
} catch (e: Exception) {
throw IllegalStateException(e.message, e)
}
}
fun run(args: Array<String>, envs: Map<String, String>, workingDir: String?) {
if (terminal.isSessionRunning) {
throw IllegalStateException(terminal.sessionName + " is already running")
}
val environment = HashMap<String, String>(envs)
if (!UIUtil.isWindows) {
environment.put("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()
}
}

View File

@ -29,7 +29,7 @@ class NodeRPC(config: NodeConfig, start: () -> Unit, invoke: (CordaRPCOps) -> Un
// Cancel the "setup" task now that we've created the RPC client. // Cancel the "setup" task now that we've created the RPC client.
this.cancel() this.cancel()
log.info("Node '{}' is now ready.", config.legalName) // Run "start-up" task, now that the RPC client is ready.
start() start()
// Schedule a new task that will refresh the display once per second. // Schedule a new task that will refresh the display once per second.

View File

@ -13,8 +13,8 @@ class DemoBenchView : View("Corda Demo Bench") {
override val root by fxml<Parent>() override val root by fxml<Parent>()
val addNodeButton by fxid<Button>() private val addNodeButton by fxid<Button>()
val nodeTabPane by fxid<TabPane>() private val nodeTabPane by fxid<TabPane>()
init { init {
importStylesheet("/net/corda/demobench/style.css") importStylesheet("/net/corda/demobench/style.css")
@ -53,4 +53,10 @@ class DemoBenchView : View("Corda Demo Bench") {
fun enableAddNodes() { fun enableAddNodes() {
addNodeButton.isDisable = false addNodeButton.isDisable = false
} }
fun forceAtLeastOneTab() {
if (nodeTabPane.tabs.isEmpty()) {
addNodeButton.fire()
}
}
} }

View File

@ -1,8 +1,10 @@
package net.corda.demobench.views package net.corda.demobench.views
import java.text.DecimalFormat import java.text.DecimalFormat
import javafx.application.Platform
import javafx.scene.control.SelectionMode.MULTIPLE import javafx.scene.control.SelectionMode.MULTIPLE
import javafx.util.converter.NumberStringConverter import javafx.util.converter.NumberStringConverter
import net.corda.demobench.model.NodeConfig
import net.corda.demobench.model.NodeController import net.corda.demobench.model.NodeController
import net.corda.demobench.model.NodeDataModel import net.corda.demobench.model.NodeDataModel
import net.corda.demobench.model.ServiceController import net.corda.demobench.model.ServiceController
@ -146,7 +148,7 @@ class NodeTabView : Fragment() {
} }
button("Create Node") { button("Create Node") {
setOnAction() { setOnAction {
if (model.validate()) { if (model.validate()) {
launch() launch()
main.enableAddNodes() main.enableAddNodes()
@ -161,23 +163,6 @@ class NodeTabView : Fragment() {
private val availableServices: List<String> private val availableServices: List<String>
get() = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries get() = if (nodeController.hasNetworkMap()) serviceController.services else serviceController.notaries
fun launch() {
model.commit()
val config = nodeController.validate(model.item)
if (config != null) {
nodeConfigView.isVisible = false
nodeTab.text = config.legalName
nodeTerminalView.open(config)
nodeTab.setOnSelectionChanged {
if (nodeTab.isSelected) {
// Doesn't work yet
nodeTerminalView.refreshTerminal()
}
}
}
}
init { init {
INTEGER_FORMAT.isGroupingUsed = false INTEGER_FORMAT.isGroupingUsed = false
@ -193,4 +178,29 @@ class NodeTabView : Fragment() {
model.webPort.value = nodeController.nextPort model.webPort.value = nodeController.nextPort
model.h2Port.value = nodeController.nextPort model.h2Port.value = nodeController.nextPort
} }
fun launch() {
model.commit()
val config = nodeController.validate(model.item)
if (config != null) {
nodeConfigView.isVisible = false
nodeTab.text = config.legalName
nodeTerminalView.open(config, onExit = { onTabClose(config) })
nodeTab.setOnSelectionChanged {
if (nodeTab.isSelected) {
// Doesn't work yet
nodeTerminalView.refreshTerminal()
}
}
}
}
private fun onTabClose(config: NodeConfig) {
Platform.runLater {
nodeTab.requestClose()
nodeController.dispose(config)
main.forceAtLeastOneTab()
}
}
} }

View File

@ -42,7 +42,7 @@ class NodeTerminalView : Fragment() {
root.vgrow = Priority.ALWAYS root.vgrow = Priority.ALWAYS
} }
fun open(config: NodeConfig) { fun open(config: NodeConfig, onExit: () -> Unit) {
nodeName.text = config.legalName nodeName.text = config.legalName
p2pPort.value = config.artemisPort.toString() p2pPort.value = config.artemisPort.toString()
@ -55,7 +55,7 @@ class NodeTerminalView : Fragment() {
root.isVisible = true root.isVisible = true
SwingUtilities.invokeLater({ SwingUtilities.invokeLater({
val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80)) val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit)
pty = r3pty pty = r3pty
swingTerminal.content = r3pty.terminal swingTerminal.content = r3pty.terminal
@ -86,12 +86,15 @@ class NodeTerminalView : Fragment() {
}) })
} }
fun enable() { fun enable(config: NodeConfig) {
config.state = NodeState.RUNNING
log.info("Node '${config.legalName}' is now ready.")
launchExplorerButton.isDisable = false launchExplorerButton.isDisable = false
viewDatabaseButton.isDisable = false viewDatabaseButton.isDisable = false
} }
fun launchRPC(config: NodeConfig) = NodeRPC(config, start = { enable() }, invoke = { ops -> fun launchRPC(config: NodeConfig) = NodeRPC(config, start = { enable(config) }, invoke = { ops ->
try { try {
val verifiedTx = ops.verifiedTransactions() val verifiedTx = ops.verifiedTransactions()
val statesInVault = ops.vaultAndUpdates() val statesInVault = ops.vaultAndUpdates()