From f1e75223a3620d3eff5e6df6849f9a7b11dde2cf Mon Sep 17 00:00:00 2001 From: Cedric Beust Date: Wed, 15 Jun 2016 03:31:59 -0800 Subject: [PATCH] First version of the WebSocket server. --- kobalt/src/Build.kt | 6 +- .../beust/kobalt/app/remote/DependencyData.kt | 18 ++++- .../beust/kobalt/app/remote/KobaltClient.kt | 58 ++++++++++++++- .../beust/kobalt/app/remote/SparkServer.kt | 74 +++++++++++++++++++ 4 files changed, 149 insertions(+), 7 deletions(-) diff --git a/kobalt/src/Build.kt b/kobalt/src/Build.kt index af5b0b66..3f87c39c 100644 --- a/kobalt/src/Build.kt +++ b/kobalt/src/Build.kt @@ -1,8 +1,7 @@ import com.beust.kobalt.TaskResult -import com.beust.kobalt.api.License -import com.beust.kobalt.api.Project -import com.beust.kobalt.api.Scm +import com.beust.kobalt.* +import com.beust.kobalt.api.* import com.beust.kobalt.api.annotation.Task import com.beust.kobalt.homeDir import com.beust.kobalt.plugin.application.application @@ -137,6 +136,7 @@ val kobaltApp = project(kobaltPluginApi, wrapper) { "com.google.code.gson:gson:${Versions.gson}", "com.squareup.retrofit2:retrofit:${Versions.retrofit}", "com.squareup.retrofit2:converter-gson:${Versions.retrofit}", + "com.squareup.okhttp3:okhttp-ws:${Versions.okhttp}", "org.codehaus.plexus:plexus-utils:3.0.22", "biz.aQute.bnd:bndlib:2.4.0", diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt b/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt index 7f612aa9..acbe643e 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/DependencyData.kt @@ -16,10 +16,15 @@ import com.google.inject.Inject import java.io.File import java.nio.file.Paths +interface IProgressListener { + fun onProgress(progress: Int? = null, message: String? = null) +} + class DependencyData @Inject constructor(val executors: KobaltExecutors, val dependencyManager: DependencyManager, val buildFileCompilerFactory: BuildFileCompiler.IFactory, val pluginInfo: PluginInfo, val taskManager: TaskManager) { - fun dependenciesDataFor(buildFilePath: String, args: Args) : GetDependenciesData { + fun dependenciesDataFor(buildFilePath: String, args: Args, progressListener: IProgressListener? = null) + : GetDependenciesData { val projectDatas = arrayListOf() fun toDependencyData(d: IClasspathDependency, scope: String): DependencyData { @@ -38,7 +43,10 @@ class DependencyData @Inject constructor(val executors: KobaltExecutors, val dep } val allTasks = hashSetOf() - projectResult.projects.forEach { project -> + projectResult.projects.withIndex().forEach { wi -> + val project = wi.value + progressListener?.onProgress(message = "Synchronizing project ${project.name} " + + (wi.index + 1) + "/" + projectResult.projects.size) val compileDependencies = pluginDependencies.map { toDependencyData(it, "compile") } + allDeps(project.compileDependencies).map { toDependencyData(it, "compile") } + allDeps(project.compileProvidedDependencies).map { toDependencyData(it, "compile") } @@ -88,5 +96,9 @@ class DependencyData @Inject constructor(val executors: KobaltExecutors, val dep class GetDependenciesData(val projects: List = emptyList(), val allTasks: Collection = emptySet(), - val errorMessage: String?) + val errorMessage: String?) { + companion object { + val NAME = "GetDependencies" + } + } } 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 237c0ccb..1d194e7c 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/KobaltClient.kt @@ -1,6 +1,7 @@ package com.beust.kobalt.app.remote import com.beust.kobalt.Args +import com.beust.kobalt.KobaltException import com.beust.kobalt.SystemProperties import com.beust.kobalt.api.Kobalt import com.beust.kobalt.app.MainModule @@ -9,11 +10,19 @@ 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 com.google.inject.Guice import com.google.inject.Inject import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ws.WebSocket +import okhttp3.ws.WebSocketCall +import okhttp3.ws.WebSocketListener +import okio.Buffer import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -28,7 +37,7 @@ import java.util.concurrent.Executors fun main(argv: Array) { Kobalt.INJECTOR = Guice.createInjector(MainModule(Args(), KobaltSettings.readSettingsXml())) - KobaltClient().run() + KobaltWebSocketClient().run() } interface Api { @@ -39,6 +48,53 @@ interface Api { fun getDependencies(@Query("buildFile") buildFile: String) : Call> } +class KobaltWebSocketClient : Runnable { + override fun run() { + val client = OkHttpClient() + val request = Request.Builder() +// .url("ws://echo.websocket.org") + .url("ws://localhost:1234/v1/getDependencies?buildFile=/Users/beust/kotlin/kobalt/kobalt/src/Build.kt") + .build() + var webSocket: WebSocket? = null + val ws = WebSocketCall.create(client, request).enqueue(object: WebSocketListener { + override fun onOpen(ws: WebSocket, response: Response) { + webSocket = ws + } + + override fun onPong(p0: Buffer?) { + println("WebSocket pong") + } + + override fun onClose(p0: Int, p1: String?) { + println("WebSocket closed") + } + + override fun onFailure(ex: IOException, response: Response?) { + ex.printStackTrace() + error("WebSocket failure: ${ex.message} response: $response") + } + + override fun onMessage(body: ResponseBody) { + val json = body.string() + val wsCommand = Gson().fromJson(json, WebSocketCommand::class.java) + if (wsCommand.errorMessage != null) { + warn("Received error message from server: " + wsCommand.errorMessage) + } else { + if (wsCommand.commandName == DependencyData.GetDependenciesData.NAME) { + val dd = Gson().fromJson(wsCommand.payload, DependencyData.GetDependenciesData::class.java) + println("Received dependency data: " + dd.projects.size + " projects") + } else if (wsCommand.commandName == ProgressCommand.NAME) { + val progress = Gson().fromJson(wsCommand.payload, ProgressCommand::class.java) + println(progress.message + (progress.progress ?: "")) + } else { + throw KobaltException("Unknown command: ${wsCommand.commandName} json:\n$json") + } + } + } + }) + } +} + class KobaltClient : Runnable { var outgoing: PrintWriter? = null diff --git a/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt b/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt index 7913852b..2396f963 100644 --- a/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt +++ b/src/main/kotlin/com/beust/kobalt/app/remote/SparkServer.kt @@ -8,6 +8,9 @@ import com.beust.kobalt.app.Templates import com.beust.kobalt.internal.PluginInfo import com.google.common.collect.ListMultimap import com.google.gson.Gson +import org.eclipse.jetty.websocket.api.RemoteEndpoint +import org.eclipse.jetty.websocket.api.Session +import org.eclipse.jetty.websocket.api.WebSocketListener import spark.ResponseTransformer import spark.Route import spark.Spark @@ -34,8 +37,12 @@ class SparkServer(val initCallback: (String) -> List, val cleanUpCallba private fun jsonRoute(path: String, route: Route) = Spark.get(path, "application/json", route, JsonTransformer()) + val log = org.slf4j.LoggerFactory.getLogger("SparkServer") + override fun run(port: Int) { + log.debug("RUNNING") Spark.port(port) + Spark.webSocket("/v1/getDependencies", GetDependenciesChatHandler::class.java) Spark.get("/ping", { req, res -> """ { "result" : "ok" } """ }) Spark.get("/quit", { req, res -> Executors.newFixedThreadPool(1).let { executor -> @@ -72,9 +79,76 @@ class SparkServer(val initCallback: (String) -> List, val cleanUpCallba jsonRoute("/v0/getTemplates", Route { request, response -> TemplatesData.create(Templates().getTemplates(pluginInfo)) }) + Spark.init() } } +class GetDependenciesChatHandler : WebSocketListener { + var session: Session? = null + + override fun onWebSocketClose(code: Int, reason: String?) { + println("ON CLOSE $code reason: $reason") + } + + override fun onWebSocketError(cause: Throwable?) { + cause?.printStackTrace() + throw UnsupportedOperationException() + } + + fun sendWebsocketCommand(endpoint: RemoteEndpoint, commandName: String, payload: T) { + endpoint.sendString(Gson().toJson(WebSocketCommand(commandName, payload = Gson().toJson(payload)))) + } + + override fun onWebSocketConnect(s: Session) { + session = s + val buildFileParams = s.upgradeRequest.parameterMap["buildFile"] + if (buildFileParams != null) { + val buildFile = buildFileParams[0] + + val result = if (buildFile != null) { + try { + val dependencyData = Kobalt.INJECTOR.getInstance(DependencyData::class.java) + val args = Kobalt.INJECTOR.getInstance(Args::class.java) + + dependencyData.dependenciesDataFor(buildFile, args, object : IProgressListener { + override fun onProgress(progress: Int?, message: String?) { + sendWebsocketCommand(s.remote, ProgressCommand.NAME, ProgressCommand(progress, message)) + } + }) + } catch(ex: Exception) { + DependencyData.GetDependenciesData(errorMessage = ex.message) + } finally { + SparkServer.cleanUpCallback() + } + } else { + DependencyData.GetDependenciesData( + errorMessage = "buildFile wasn't passed in the query parameter") + } + println("GOT DEPENDENCY DATA: $result") + sendWebsocketCommand(s.remote, DependencyData.GetDependenciesData.NAME, result) + s.close() + } + } + + override fun onWebSocketText(message: String?) { + println("RECEIVED TEXT: $message") + session?.remote?.sendString("Response: $message") + } + + override fun onWebSocketBinary(payload: ByteArray?, offset: Int, len: Int) { + println("RECEIVED BINARY: $payload") + } + +} + +class ProgressCommand(val progress: Int? = null, val message: String? = null) { + companion object { + val NAME = "ProgressCommand" + } +} + +class WebSocketCommand(val commandName: String, val errorMessage: String? = null, val payload: String) + class TemplateData(val pluginName: String, val templates: List) class TemplatesData(val templates: List) {