From bace815481af3066d0baf41548cd7f39e741ff38 Mon Sep 17 00:00:00 2001 From: Cedric Beust Date: Mon, 25 Apr 2016 02:04:17 -0800 Subject: [PATCH] Make Kobalt reentrant. --- .../src/main/kotlin/com/beust/kobalt/Args.kt | 4 + .../kotlin/com/beust/kobalt/api/Kobalt.kt | 15 +++ src/main/kotlin/com/beust/kobalt/Main.kt | 22 ++-- .../beust/kobalt/app/remote/KobaltClient.kt | 116 +++++++++++++++++- .../com/beust/kobalt/app/remote/KobaltHub.kt | 39 ++++++ .../beust/kobalt/app/remote/KobaltServer.kt | 41 ++++--- .../beust/kobalt/app/remote/ProcessUtil.kt | 27 ++++ 7 files changed, 228 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt create mode 100644 src/main/kotlin/com/beust/kobalt/app/remote/ProcessUtil.kt diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt index dbc71200..42d6ff05 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/Args.kt @@ -26,6 +26,10 @@ class Args { "actually running them") var dryRun: Boolean = false + @Parameter(names = arrayOf("--force"), description = "Force a new server to be launched even if another one" + + " is already running") + var force: Boolean = false + @Parameter(names = arrayOf("--gc"), description = "Delete old files") var gc: Boolean = false diff --git a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt index 493fa845..2b96dbcb 100644 --- a/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt +++ b/modules/kobalt-plugin-api/src/main/kotlin/com/beust/kobalt/api/Kobalt.kt @@ -6,6 +6,9 @@ import com.beust.kobalt.Plugins import com.beust.kobalt.ProxyConfig import com.google.inject.Injector import org.eclipse.aether.repository.Proxy +import com.beust.kobalt.internal.PluginInfo +import com.google.inject.Guice +import com.google.inject.Module import java.io.InputStream import java.net.InetSocketAddress import java.time.Duration @@ -15,6 +18,18 @@ class Kobalt { companion object { lateinit var INJECTOR : Injector + fun init(module: Module) { + Kobalt.INJECTOR = Guice.createInjector(module) + + // + // Add all the plugins read in kobalt-plugin.xml to the Plugins singleton, so that code + // in the build file that calls Plugins.findPlugin() can find them (code in the + // build file do not have access to the KobaltContext). + // + val pluginInfo = Kobalt.INJECTOR.getInstance(PluginInfo::class.java) + pluginInfo.plugins.forEach { Plugins.addPluginInstance(it) } + } + var context: KobaltContext? = null val proxyConfig = with(Kobalt.context?.settings?.proxy) { diff --git a/src/main/kotlin/com/beust/kobalt/Main.kt b/src/main/kotlin/com/beust/kobalt/Main.kt index 5791e39e..df88e06a 100644 --- a/src/main/kotlin/com/beust/kobalt/Main.kt +++ b/src/main/kotlin/com/beust/kobalt/Main.kt @@ -6,6 +6,7 @@ import com.beust.kobalt.api.Kobalt import com.beust.kobalt.api.PluginTask import com.beust.kobalt.api.Project import com.beust.kobalt.app.* +import com.beust.kobalt.app.remote.DependencyData import com.beust.kobalt.app.remote.KobaltClient import com.beust.kobalt.app.remote.KobaltServer import com.beust.kobalt.internal.Gc @@ -18,7 +19,6 @@ import com.beust.kobalt.maven.Http import com.beust.kobalt.maven.dependency.FileDependency import com.beust.kobalt.misc.* import com.google.common.collect.HashMultimap -import com.google.inject.Guice import java.io.File import java.net.URLClassLoader import java.nio.file.Paths @@ -42,7 +42,7 @@ private fun parseArgs(argv: Array): Main.RunInfo { fun mainNoExit(argv: Array): Int { val (jc, args) = parseArgs(argv) - Kobalt.INJECTOR = Guice.createInjector(MainModule(args, KobaltSettings.readSettingsXml())) + Kobalt.init(MainModule(args, KobaltSettings.readSettingsXml())) val result = Kobalt.INJECTOR.getInstance(Main::class.java).run { val runResult = run(jc, args, argv) pluginInfo.shutdown() @@ -64,8 +64,8 @@ private class Main @Inject constructor( val github: GithubApi2, val updateKobalt: UpdateKobalt, val client: KobaltClient, - val server: KobaltServer, val pluginInfo: PluginInfo, + val dependencyData: DependencyData, val projectGenerator: ProjectGenerator, val resolveDependency: ResolveDependency) { @@ -91,21 +91,12 @@ private class Main @Inject constructor( } fun run(jc: JCommander, args: Args, argv: Array): Int { + // // Install plug-ins requested from the command line // val pluginClassLoader = installCommandLinePlugins(args) - // - // Add all the plugins read in kobalt-plugin.xml to the Plugins singleton, so that code - // in the build file that calls Plugins.findPlugin() can find them (code in the - // build file do not have access to the KobaltContext). - // - pluginInfo.plugins.forEach { Plugins.addPluginInstance(it) } - -// val data = dependencyData.dependenciesDataFor(homeDir("kotlin/klaxon/kobalt/src/Build.kt"), Args()) -// println("Data: $data") - // --listTemplates if (args.listTemplates) { Templates().list(pluginInfo) @@ -166,7 +157,7 @@ private class Main @Inject constructor( } else if (args.usage) { jc.usage() } else if (args.serverMode) { - server.run() + val port = KobaltServer(args.force, { pluginInfo.shutdown()}).call() } else { // Options that don't need Build.kt to be parsed first if (args.gc) { @@ -213,6 +204,9 @@ private class Main @Inject constructor( // plugins.applyPlugins(Kobalt.context!!, allProjects) + // DONOTCOMMIT +// val data = dependencyData.dependenciesDataFor(homeDir("kotlin/klaxon/kobalt/src/Build.kt"), Args()) +// println("Data: $data") if (args.projectInfo) { // --projectInfo diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt index f381b4e7..e3712918 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt @@ -1,18 +1,128 @@ package com.beust.kobalt.app.remote +import com.beust.kobalt.Args import com.beust.kobalt.SystemProperties +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.app.MainModule +import com.beust.kobalt.homeDir +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.misc.KFiles import com.beust.kobalt.misc.log +import com.beust.kobalt.misc.warn import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonParser -import java.io.BufferedReader -import java.io.InputStreamReader -import java.io.PrintWriter +import com.google.inject.Guice +import java.io.* import java.net.ConnectException import java.net.Socket import java.nio.file.Paths +import java.util.* +import java.util.concurrent.Executors import javax.inject.Inject +fun main(argv: Array) { + Kobalt.INJECTOR = Guice.createInjector(MainModule(Args(), KobaltSettings.readSettingsXml())) + val port = ServerProcess().launch() + println("SERVER RUNNING ON PORT $port") +} + +class ServerProcess { + val SERVER_FILE = KFiles.joinDir(homeDir(KFiles.KOBALT_DOT_DIR, "kobaltServer.properties")) + val KEY_PORT = "port" + val executor = Executors.newFixedThreadPool(5) + + fun launch() : Int { + var port = launchPrivate() + while (port == 0) { + executor.submit { + KobaltServer(force = true, shutdownCallback = {}).call() + } + // launchServer(ProcessUtil.findAvailablePort()) + port = launchPrivate() + } + return port + } + + private fun launchPrivate() : Int { + var result = 0 + File(SERVER_FILE).let { serverFile -> + if (serverFile.exists()) { + val properties = Properties().apply { + load(FileReader(serverFile)) + } + + try { + val socket = Socket("localhost", result) + val outgoing = PrintWriter(socket.outputStream, true) + val c: String = """{ "name": "ping"}""" + outgoing.println(c) + val ins = BufferedReader(InputStreamReader(socket.inputStream)) + var line = ins.readLine() + val jo = JsonParser().parse(line) as JsonObject + val jsonData = jo["data"]?.asString + val dataObject = JsonParser().parse(jsonData) as JsonObject + val received = JsonParser().parse(dataObject["received"].asString) as JsonObject + if (received["name"].asString == "ping") { + result = properties.getProperty(KEY_PORT).toInt() + } + } catch(ex: IOException) { + log(1, "Couldn't connect to current server, launching a new one") + Thread.sleep(1000) + } + } + } + + return result + } + + private fun launchServer(port: Int) { + val kobaltJar = File(KFiles().kobaltJar[0]) + log(1, "Kobalt jar: $kobaltJar") + if (! kobaltJar.exists()) { + warn("Can't find the jar file " + kobaltJar.absolutePath + " can't be found") + } else { + val args = listOf("java", + "-classpath", KFiles().kobaltJar.joinToString(File.pathSeparator), + "com.beust.kobalt.MainKt", + "--dev", "--server", "--port", port.toString()) + val pb = ProcessBuilder(args) +// pb.directory(File(directory)) + pb.inheritIO() +// pb.environment().put("JAVA_HOME", ProjectJdkTable.getInstance().allJdks[0].homePath) + val tempFile = createTempFile("kobalt") + pb.redirectOutput(tempFile) + warn("Launching " + args.joinToString(" ")) + warn("Server output in: $tempFile") + val process = pb.start() + val errorCode = process.waitFor() + if (errorCode == 0) { + log(1, "Server exiting") + } else { + log(1, "Server exiting with error") + } + } + } + + private fun createServerFile(port: Int, force: Boolean) : Boolean { + if (File(SERVER_FILE).exists() && ! force) { + log(1, "Server file $SERVER_FILE already exists, is another server running?") + return false + } else { + Properties().apply { + put(KEY_PORT, port.toString()) + }.store(FileWriter(SERVER_FILE), "") + log(2, "KobaltServer created $SERVER_FILE") + return true + } + } + + private fun deleteServerFile() { + log(1, "KobaltServer deleting $SERVER_FILE") + File(SERVER_FILE).delete() + } +} + class KobaltClient @Inject constructor() : Runnable { var outgoing: PrintWriter? = null diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt new file mode 100644 index 00000000..8787646e --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltHub.kt @@ -0,0 +1,39 @@ +package com.beust.kobalt.app.remote + +import com.beust.kobalt.Args +import com.beust.kobalt.api.Kobalt +import com.beust.kobalt.app.MainModule +import com.beust.kobalt.internal.KobaltSettings +import com.beust.kobalt.internal.remote.ICommand +import com.google.gson.Gson + +enum class Command(val n: Int, val command: ICommand) { + GET_DEPENDENCIES(1, Kobalt.INJECTOR.getInstance(GetDependenciesCommand::class.java)); + companion object { + val commandMap = hashMapOf() + fun commandFor(n: Int) = values().filter { it.n == n }[0].command + } +} + +class KobaltHub(val dependencyData: DependencyData) { + val args = Args() + + fun runCommand(n: Int) : String { + val data = + when(n) { + 1 -> Gson().toJson( + dependencyData.dependenciesDataFor("/Users/beust/kotlin/klaxon/kobalt/src/Build.kt", args)) + else -> throw RuntimeException("Unknown command") + } + println("Data: $data") + return data + } +} + +fun main(argv: Array) { + Kobalt.init(MainModule(Args(), KobaltSettings.readSettingsXml())) + val dependencyData = Kobalt.INJECTOR.getInstance(DependencyData::class.java) + val json = KobaltHub(dependencyData).runCommand(1) + val dd = Gson().fromJson(json, DependencyData.GetDependenciesData::class.java) + println("Data2: $dd") +} diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltServer.kt b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltServer.kt index 766caad0..6973e362 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltServer.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltServer.kt @@ -1,9 +1,7 @@ package com.beust.kobalt.app.remote -import com.beust.kobalt.Args import com.beust.kobalt.api.Kobalt import com.beust.kobalt.homeDir -import com.beust.kobalt.internal.PluginInfo import com.beust.kobalt.internal.remote.CommandData import com.beust.kobalt.internal.remote.ICommandSender import com.beust.kobalt.internal.remote.PingCommand @@ -12,15 +10,14 @@ import com.beust.kobalt.misc.log import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonParser -import com.google.inject.Singleton import java.io.* +import java.lang.management.ManagementFactory import java.net.ServerSocket import java.net.SocketException import java.util.* -import javax.inject.Inject +import java.util.concurrent.Callable -@Singleton -class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInfo) : Runnable, ICommandSender { +class KobaltServer(val force: Boolean, val shutdownCallback: () -> Unit) : Callable, ICommandSender { // var outgoing: PrintWriter? = null val pending = arrayListOf() @@ -29,28 +26,34 @@ class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInf Kobalt.INJECTOR.getInstance(it).let { Pair(it.name, it) } }.toMap() - override fun run() { + override fun call() : Int { + val port = ProcessUtil.findAvailablePort() try { - if (createServerFile(args.port)) { - privateRun() + if (createServerFile(port, force)) { + privateRun(port) } } catch(ex: Exception) { ex.printStackTrace() } finally { deleteServerFile() } + return port } val SERVER_FILE = KFiles.joinDir(homeDir(KFiles.KOBALT_DOT_DIR, "kobaltServer.properties")) val KEY_PORT = "port" + val KEY_PID = "pid" - private fun createServerFile(port: Int) : Boolean { - if (File(SERVER_FILE).exists()) { + private fun createServerFile(port: Int, force: Boolean) : Boolean { + if (File(SERVER_FILE).exists() && ! force) { log(1, "Server file $SERVER_FILE already exists, is another server running?") return false } else { + val processName = ManagementFactory.getRuntimeMXBean().name + val pid = processName.split("@")[0] Properties().apply { put(KEY_PORT, port.toString()) + put(KEY_PID, pid) }.store(FileWriter(SERVER_FILE), "") log(2, "KobaltServer created $SERVER_FILE") return true @@ -84,12 +87,10 @@ class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInf } } - private fun privateRun() { - val portNumber = args.port - - log(1, "Listening to port $portNumber") + private fun privateRun(port: Int) { + log(1, "Listening to port $port") var quit = false - serverInfo = ServerInfo(portNumber) + serverInfo = ServerInfo(port) while (!quit) { if (pending.size > 0) { log(1, "Emptying the queue, size $pending.size()") @@ -114,10 +115,13 @@ class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInf // Done, send a quit to the client sendData(CommandData("quit", "")) + // Clean up all the plug-in actors + shutdownCallback() line = serverInfo.reader.readLine() } } if (line == null) { + log(1, "Received null line, resetting the server") serverInfo.reset() } } catch(ex: SocketException) { @@ -130,8 +134,6 @@ class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInf } log(1, "Command failed: ${ex.message}") } - - pluginInfo.shutdown() } } @@ -155,4 +157,5 @@ class KobaltServer @Inject constructor(val args: Args, val pluginInfo: PluginInf } } } -} \ No newline at end of file +} + diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/ProcessUtil.kt b/src/main/kotlin/com/beust/kobalt/app/remote/ProcessUtil.kt new file mode 100644 index 00000000..982f8091 --- /dev/null +++ b/src/main/kotlin/com/beust/kobalt/app/remote/ProcessUtil.kt @@ -0,0 +1,27 @@ +package com.beust.kobalt.app.remote + +import java.io.IOException +import java.net.Socket + +class ProcessUtil { + companion object { + fun findAvailablePort(): Int { + for (i in 1234..65000) { + if (isPortAvailable(i)) return i + } + throw IllegalArgumentException("Couldn't find any port available, something is very wrong") + } + + private fun isPortAvailable(port: Int): Boolean { + var s: Socket? = null + try { + s = Socket("localhost", port) + return false + } catch(ex: IOException) { + return true + } finally { + s?.close() + } + } + } +} \ No newline at end of file