NodeRunner enhancements (#614)

* Use same java.home as caller in terminal window case, previously only headless case did that
* Bump gradlePluginsVersion as I've changed NodeRunner

On Linux:
* Prefer JAVA_HOME over PATH
* Use correct escaping
* Start sh on failure so that terminals don't disappear taking error messages with them
This commit is contained in:
Andrzej Cichocki 2017-05-09 10:22:22 +01:00 committed by GitHub
parent ccb8827107
commit 45997ccd13
3 changed files with 70 additions and 81 deletions

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=0.11.2 gradlePluginsVersion=0.12.0
kotlinVersion=1.1.1 kotlinVersion=1.1.1
guavaVersion=21.0 guavaVersion=21.0
bouncycastleVersion=1.56 bouncycastleVersion=1.56

View File

@ -9,5 +9,5 @@ cd "$basedir"
if which osascript >/dev/null; then if which osascript >/dev/null; then
/usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@" /usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@"
else else
java -jar runnodes.jar "$@" "${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar runnodes.jar "$@"
fi fi

View File

@ -3,119 +3,108 @@ package net.corda.plugins
import java.awt.GraphicsEnvironment import java.awt.GraphicsEnvironment
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths
import java.util.* import java.util.*
private val nodeJarName = "corda.jar"
private val webJarName = "corda-webserver.jar"
private val nodeConfName = "node.conf"
private val HEADLESS_FLAG = "--headless" private val HEADLESS_FLAG = "--headless"
private val os: OS by lazy { private val os by lazy {
val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH)
if ((osName.indexOf("mac") >= 0) || (osName.indexOf("darwin") >= 0)) OS.MACOS if ("mac" in osName || "darwin" in osName) OS.MACOS
else if (osName.indexOf("win") >= 0) OS.WINDOWS else if ("win" in osName) OS.WINDOWS
else OS.LINUX else OS.LINUX
} }
private enum class OS { MACOS, WINDOWS, LINUX } private enum class OS { MACOS, WINDOWS, LINUX }
data class IncrementalPortAllocator(var basePort: Int = 5005) { private object debugPortAlloc {
fun next(): Int = basePort++ private var basePort = 5005
internal fun next() = basePort++
} }
val debugPortAlloc: IncrementalPortAllocator = IncrementalPortAllocator()
fun main(args: Array<String>) { fun main(args: Array<String>) {
val startedProcesses = mutableListOf<Process>() val startedProcesses = mutableListOf<Process>()
val headless = GraphicsEnvironment.isHeadless() || (args.isNotEmpty() && args[0] == HEADLESS_FLAG) val headless = GraphicsEnvironment.isHeadless() || (args.isNotEmpty() && args[0] == HEADLESS_FLAG)
val workingDir = Paths.get(System.getProperty("user.dir")).toFile() val workingDir = File(System.getProperty("user.dir"))
val javaArgs = args.filter { it != HEADLESS_FLAG } val javaArgs = args.filter { it != HEADLESS_FLAG }
println("Starting nodes in $workingDir") println("Starting nodes in $workingDir")
workingDir.listFiles { file -> file.isDirectory }.forEach { dir ->
workingDir.list().map { File(workingDir, it) }.forEach { listOf(NodeJarType, WebJarType).forEach { jarType ->
if (isNode(it)) { jarType.acceptDirAndStartProcess(dir, headless, javaArgs)?.let { startedProcesses += it }
startedProcesses += startJarProcess(headless, it, nodeJarName, javaArgs)
}
if (isWebserver(it)) {
startedProcesses += startJarProcess(headless, it, webJarName, javaArgs)
} }
} }
println("Started ${startedProcesses.size} processes") println("Started ${startedProcesses.size} processes")
println("Finished starting nodes") println("Finished starting nodes")
} }
private fun startJarProcess(headless: Boolean, dir: File, jarName: String, javaArgs: List<String>) : Process { private abstract class JarType(private val jarName: String) {
val runJar = getJarRunner(headless) internal abstract fun acceptNodeConf(nodeConf: File): Boolean
val debugPort = debugPortAlloc.next() internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List<String>): Process? {
println("Starting $jarName in $dir on debug port $debugPort") if (!File(dir, jarName).exists()) {
val proc = runJar(jarName, dir, javaArgs, debugPort) return null
if (os == OS.MACOS) Thread.sleep(1000) }
return proc if (!File(dir, "node.conf").let { it.exists() && acceptNodeConf(it) }) {
return null
}
val debugPort = debugPortAlloc.next()
println("Starting $jarName in $dir on debug port $debugPort")
val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, javaArgs).start()
if (os == OS.MACOS) Thread.sleep(1000)
return process
}
} }
private fun isNode(maybeNodeDir: File) = maybeNodeDir.isDirectory private object NodeJarType : JarType("corda.jar") {
&& File(maybeNodeDir, nodeJarName).exists() override fun acceptNodeConf(nodeConf: File) = true
&& File(maybeNodeDir, nodeConfName).exists()
private fun isWebserver(maybeWebserverDir: File) = maybeWebserverDir.isDirectory
&& File(maybeWebserverDir, webJarName).exists()
&& File(maybeWebserverDir, nodeConfName).exists()
&& hasWebserverPort(maybeWebserverDir)
// TODO: Add a webserver.conf, or use TypeSafe config instead of this hack
private fun hasWebserverPort(nodeConfDir: File) = Files.readAllLines(File(nodeConfDir, nodeConfName).toPath()).joinToString { it }.contains("webAddress")
private fun getDebugPortArg(debugPort: Int?) = if (debugPort != null) {
listOf("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
} else {
emptyList()
} }
private fun getJarRunner(headless: Boolean): (String, File, List<String>, Int?) -> Process = if (headless) ::execJar else ::execJarInTerminalWindow private object WebJarType : JarType("corda-webserver.jar") {
// TODO: Add a webserver.conf, or use TypeSafe config instead of this hack
private fun execJar(jarName: String, dir: File, args: List<String> = listOf(), debugPort: Int?): Process { override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it }
val nodeName = dir.toPath().fileName
val separator = System.getProperty("file.separator")
val path = System.getProperty("java.home") + separator + "bin" + separator + "java"
val builder = ProcessBuilder(listOf(path, "-Dname=$nodeName") + getDebugPortArg(debugPort) + listOf("-jar", jarName, "--no-local-shell") + args)
builder.redirectError(Paths.get("error.${dir.toPath().fileName}.log").toFile())
builder.inheritIO()
builder.directory(dir)
return builder.start()
} }
private fun execJarInTerminalWindow(jarName: String, dir: File, args: List<String> = listOf(), debugPort: Int?): Process { private abstract class JavaCommand(jarName: String, internal val dir: File, debugPort: Int?, internal val nodeName: String, init: MutableList<String>.() -> Unit, args: List<String>) {
val nodeName = "${dir.toPath().fileName}-$jarName" internal val command: List<String> = mutableListOf<String>().apply {
val javaCmd = (listOf("java", "-Dname=$nodeName") + getDebugPortArg(debugPort) + listOf("-jar", jarName) + args).joinToString(" ") { it } add(File(File(System.getProperty("java.home"), "bin"), "java").path)
val builder = when (os) { add("-Dname=$nodeName")
OS.MACOS -> ProcessBuilder( null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
"osascript", "-e", add("-jar"); add(jarName)
"""tell app "Terminal" init()
addAll(args)
}
internal abstract fun processBuilder(): ProcessBuilder
internal fun start() = processBuilder().directory(dir).start()
}
private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>) : JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args) {
override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO()
}
private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List<String>) : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args) {
override fun processBuilder() = ProcessBuilder(when (os) {
OS.MACOS -> {
listOf("osascript", "-e", """tell app "Terminal"
activate activate
tell app "System Events" to tell process "Terminal" to keystroke "t" using command down tell app "System Events" to tell process "Terminal" to keystroke "t" using command down
delay 0.5 delay 0.5
do script "bash -c 'cd $dir; /usr/libexec/java_home -v 1.8 --exec $javaCmd && exit'" in selected tab of the front window do script "bash -c 'cd $dir; /usr/libexec/java_home -v 1.8 --exec ${command.joinToString(" ")} && exit'" in selected tab of the front window
end tell""" end tell""")
) }
OS.WINDOWS -> ProcessBuilder( OS.WINDOWS -> {
"cmd" listOf("cmd", "/C", "start ${command.joinToString(" ")}")
, "/C", "start $javaCmd" }
)
OS.LINUX -> { OS.LINUX -> {
val isTmux = System.getenv("TMUX")?.isNotEmpty() ?: false val command = "${unixCommand()} || sh"
if (isTmux) { if (isTmux()) {
ProcessBuilder( listOf("tmux", "new-window", "-n", nodeName, command)
"tmux", "new-window", "-n", nodeName, javaCmd
)
} else { } else {
ProcessBuilder( listOf("xterm", "-T", nodeName, "-e", command)
"xterm", "-T", nodeName, "-e", javaCmd
)
} }
} }
} })
return builder.directory(dir).start()
private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ")
} }
private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells.
private fun isTmux() = System.getenv("TMUX")?.isNotEmpty() ?: false