From 45997ccd13741fc0a103eee224488212b8f39d6b Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Tue, 9 May 2017 10:22:22 +0100 Subject: [PATCH] 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 --- constants.properties | 2 +- .../main/resources/net/corda/plugins/runnodes | 2 +- .../kotlin/net/corda/plugins/NodeRunner.kt | 147 ++++++++---------- 3 files changed, 70 insertions(+), 81 deletions(-) diff --git a/constants.properties b/constants.properties index 0498a1631e..8693758e20 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.11.2 +gradlePluginsVersion=0.12.0 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.56 diff --git a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes index b18f540b64..9e3ba4c5be 100644 --- a/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes +++ b/gradle-plugins/cordformation/src/main/resources/net/corda/plugins/runnodes @@ -9,5 +9,5 @@ cd "$basedir" if which osascript >/dev/null; then /usr/libexec/java_home -v 1.8 --exec java -jar runnodes.jar "$@" else - java -jar runnodes.jar "$@" + "${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar runnodes.jar "$@" fi diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index aeb57a3009..907a9126a4 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -3,119 +3,108 @@ package net.corda.plugins import java.awt.GraphicsEnvironment import java.io.File import java.nio.file.Files -import java.nio.file.Paths 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 os: OS by lazy { +private val os by lazy { val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) - if ((osName.indexOf("mac") >= 0) || (osName.indexOf("darwin") >= 0)) OS.MACOS - else if (osName.indexOf("win") >= 0) OS.WINDOWS + if ("mac" in osName || "darwin" in osName) OS.MACOS + else if ("win" in osName) OS.WINDOWS else OS.LINUX } private enum class OS { MACOS, WINDOWS, LINUX } -data class IncrementalPortAllocator(var basePort: Int = 5005) { - fun next(): Int = basePort++ +private object debugPortAlloc { + private var basePort = 5005 + internal fun next() = basePort++ } -val debugPortAlloc: IncrementalPortAllocator = IncrementalPortAllocator() - fun main(args: Array) { val startedProcesses = mutableListOf() 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 } println("Starting nodes in $workingDir") - - workingDir.list().map { File(workingDir, it) }.forEach { - if (isNode(it)) { - startedProcesses += startJarProcess(headless, it, nodeJarName, javaArgs) - } - - if (isWebserver(it)) { - startedProcesses += startJarProcess(headless, it, webJarName, javaArgs) + workingDir.listFiles { file -> file.isDirectory }.forEach { dir -> + listOf(NodeJarType, WebJarType).forEach { jarType -> + jarType.acceptDirAndStartProcess(dir, headless, javaArgs)?.let { startedProcesses += it } } } - println("Started ${startedProcesses.size} processes") println("Finished starting nodes") } -private fun startJarProcess(headless: Boolean, dir: File, jarName: String, javaArgs: List) : Process { - val runJar = getJarRunner(headless) - val debugPort = debugPortAlloc.next() - println("Starting $jarName in $dir on debug port $debugPort") - val proc = runJar(jarName, dir, javaArgs, debugPort) - if (os == OS.MACOS) Thread.sleep(1000) - return proc +private abstract class JarType(private val jarName: String) { + internal abstract fun acceptNodeConf(nodeConf: File): Boolean + internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List): Process? { + if (!File(dir, jarName).exists()) { + return null + } + 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 - && File(maybeNodeDir, nodeJarName).exists() - && 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 object NodeJarType : JarType("corda.jar") { + override fun acceptNodeConf(nodeConf: File) = true } -private fun getJarRunner(headless: Boolean): (String, File, List, Int?) -> Process = if (headless) ::execJar else ::execJarInTerminalWindow - -private fun execJar(jarName: String, dir: File, args: List = listOf(), debugPort: Int?): Process { - 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 object WebJarType : JarType("corda-webserver.jar") { + // TODO: Add a webserver.conf, or use TypeSafe config instead of this hack + override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it } } -private fun execJarInTerminalWindow(jarName: String, dir: File, args: List = listOf(), debugPort: Int?): Process { - val nodeName = "${dir.toPath().fileName}-$jarName" - val javaCmd = (listOf("java", "-Dname=$nodeName") + getDebugPortArg(debugPort) + listOf("-jar", jarName) + args).joinToString(" ") { it } - val builder = when (os) { - OS.MACOS -> ProcessBuilder( - "osascript", "-e", - """tell app "Terminal" +private abstract class JavaCommand(jarName: String, internal val dir: File, debugPort: Int?, internal val nodeName: String, init: MutableList.() -> Unit, args: List) { + internal val command: List = mutableListOf().apply { + add(File(File(System.getProperty("java.home"), "bin"), "java").path) + add("-Dname=$nodeName") + null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") + add("-jar"); add(jarName) + 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) : 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) : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args) { + override fun processBuilder() = ProcessBuilder(when (os) { + OS.MACOS -> { + listOf("osascript", "-e", """tell app "Terminal" activate tell app "System Events" to tell process "Terminal" to keystroke "t" using command down 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 -end tell""" - ) - OS.WINDOWS -> ProcessBuilder( - "cmd" - , "/C", "start $javaCmd" - ) + 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""") + } + OS.WINDOWS -> { + listOf("cmd", "/C", "start ${command.joinToString(" ")}") + } OS.LINUX -> { - val isTmux = System.getenv("TMUX")?.isNotEmpty() ?: false - if (isTmux) { - ProcessBuilder( - "tmux", "new-window", "-n", nodeName, javaCmd - ) + val command = "${unixCommand()} || sh" + if (isTmux()) { + listOf("tmux", "new-window", "-n", nodeName, command) } else { - ProcessBuilder( - "xterm", "-T", nodeName, "-e", javaCmd - ) + listOf("xterm", "-T", nodeName, "-e", command) } } - } - 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