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")
init {
log.info("Explorer JAR: " + explorerPath)
log.info("Explorer JAR: $explorerPath")
}
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 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 {
log.info("Java executable: " + javaPath)
log.info("Java executable: $javaPath")
}
fun commandFor(jarPath: Path, vararg args: String): Array<String> {

View File

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

View File

@ -38,8 +38,8 @@ class NodeController : Controller() {
private var networkMapConfig: NetworkMapConfig? = null
init {
log.info("Base directory: " + baseDir)
log.info("Corda JAR: " + cordaPath)
log.info("Base directory: $baseDir")
log.info("Corda JAR: $cordaPath")
}
fun validate(nodeData: NodeData): NodeConfig? {
@ -54,7 +54,7 @@ class NodeController : Controller() {
)
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
}
@ -64,6 +64,14 @@ class NodeController : Controller() {
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 {
@ -90,7 +98,7 @@ class NodeController : Controller() {
config.networkMap = networkMapConfig
} else {
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
pty.run(command, System.getenv(), nodeDir.toString())
log.info("Launched node: " + config.legalName)
log.info("Launched node: ${config.legalName}")
return true
} catch (e: Exception) {
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()
set.add(service)
log.info("Supports: " + service)
log.info("Supports: $service")
}
}
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.
this.cancel()
log.info("Node '{}' is now ready.", config.legalName)
// Run "start-up" task, now that the RPC client is ready.
start()
// 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>()
val addNodeButton by fxid<Button>()
val nodeTabPane by fxid<TabPane>()
private val addNodeButton by fxid<Button>()
private val nodeTabPane by fxid<TabPane>()
init {
importStylesheet("/net/corda/demobench/style.css")
@ -53,4 +53,10 @@ class DemoBenchView : View("Corda Demo Bench") {
fun enableAddNodes() {
addNodeButton.isDisable = false
}
fun forceAtLeastOneTab() {
if (nodeTabPane.tabs.isEmpty()) {
addNodeButton.fire()
}
}
}

View File

@ -1,8 +1,10 @@
package net.corda.demobench.views
import java.text.DecimalFormat
import javafx.application.Platform
import javafx.scene.control.SelectionMode.MULTIPLE
import javafx.util.converter.NumberStringConverter
import net.corda.demobench.model.NodeConfig
import net.corda.demobench.model.NodeController
import net.corda.demobench.model.NodeDataModel
import net.corda.demobench.model.ServiceController
@ -146,7 +148,7 @@ class NodeTabView : Fragment() {
}
button("Create Node") {
setOnAction() {
setOnAction {
if (model.validate()) {
launch()
main.enableAddNodes()
@ -161,23 +163,6 @@ class NodeTabView : Fragment() {
private val availableServices: List<String>
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 {
INTEGER_FORMAT.isGroupingUsed = false
@ -193,4 +178,29 @@ class NodeTabView : Fragment() {
model.webPort.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
}
fun open(config: NodeConfig) {
fun open(config: NodeConfig, onExit: () -> Unit) {
nodeName.text = config.legalName
p2pPort.value = config.artemisPort.toString()
@ -55,7 +55,7 @@ class NodeTerminalView : Fragment() {
root.isVisible = true
SwingUtilities.invokeLater({
val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80))
val r3pty = R3Pty(config.legalName, TerminalSettingsProvider(), Dimension(160, 80), onExit)
pty = r3pty
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
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 {
val verifiedTx = ops.verifiedTransactions()
val statesInVault = ops.vaultAndUpdates()