From cd9f0d271171b3cc8554ba36001f8f2c070b2c1d Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 28 Feb 2018 05:39:06 -0500 Subject: [PATCH 1/3] make sbt server extensible Fixes #3890 Here's an example: ```scala Global / serverHandlers += ServerHandler({ callback => import callback._ import sjsonnew.BasicJsonProtocol._ import sbt.internal.protocol.JsonRpcRequestMessage ServerIntent( { case r: JsonRpcRequestMessage if r.method == "lunar/helo" => jsonRpcNotify("lunar/oleh", "") () }, PartialFunction.empty ) ``` --- build.sbt | 1 + .../src/main/scala/sbt/BasicKeys.scala | 5 + .../src/main/scala/sbt/ServerHandler.scala | 67 ++++++++ main/src/main/scala/sbt/Defaults.scala | 8 +- main/src/main/scala/sbt/Keys.scala | 2 + main/src/main/scala/sbt/Project.scala | 3 + .../scala/sbt/internal/CommandExchange.scala | 6 +- .../server/LanguageServerProtocol.scala | 155 ++++++++++-------- .../sbt/internal/server/NetworkChannel.scala | 31 +++- sbt/src/server-test/handshake/build.sbt | 15 ++ sbt/src/test/scala/sbt/ServerSpec.scala | 37 +++-- 11 files changed, 244 insertions(+), 86 deletions(-) create mode 100644 main-command/src/main/scala/sbt/ServerHandler.scala diff --git a/build.sbt b/build.sbt index 8de457067..a5f333fed 100644 --- a/build.sbt +++ b/build.sbt @@ -498,6 +498,7 @@ lazy val sbtProj = (project in file("sbt")) connectInput in run in Test := true, outputStrategy in run in Test := Some(StdoutOutput), fork in Test := true, + parallelExecution in Test := false, ) .configure(addSbtCompilerBridge) diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index 1570d392b..393397f3f 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -39,6 +39,11 @@ object BasicKeys { "The wire protocol for the server command.", 10000) + val fullServerHandlers = + AttributeKey[Seq[ServerHandler]]("fullServerHandlers", + "Combines default server handlers and user-defined handlers.", + 10000) + val autoStartServer = AttributeKey[Boolean]( "autoStartServer", diff --git a/main-command/src/main/scala/sbt/ServerHandler.scala b/main-command/src/main/scala/sbt/ServerHandler.scala new file mode 100644 index 000000000..07fe8616e --- /dev/null +++ b/main-command/src/main/scala/sbt/ServerHandler.scala @@ -0,0 +1,67 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt + +import sjsonnew.JsonFormat +import sbt.internal.protocol._ +import sbt.util.Logger +import sbt.protocol.{ SettingQuery => Q } + +/** + * ServerHandler allows plugins to extend sbt server. + * It's a wrapper around curried function ServerCallback => JsonRpcRequestMessage => Unit. + */ +final class ServerHandler(val handler: ServerCallback => ServerIntent) { + override def toString: String = s"Serverhandler(...)" +} + +object ServerHandler { + def apply(handler: ServerCallback => ServerIntent): ServerHandler = + new ServerHandler(handler) + + lazy val fallback: ServerHandler = ServerHandler({ handler => + ServerIntent( + { case x => handler.log.debug(s"Unhandled notification received: ${x.method}") }, + { case x => handler.log.debug(s"Unhandled request received: ${x.method}") } + ) + }) +} + +final class ServerIntent(val onRequest: PartialFunction[JsonRpcRequestMessage, Unit], + val onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]) { + override def toString: String = s"ServerIntent(...)" +} + +object ServerIntent { + def apply(onRequest: PartialFunction[JsonRpcRequestMessage, Unit], + onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]): ServerIntent = + new ServerIntent(onRequest, onNotification) + + def request(onRequest: PartialFunction[JsonRpcRequestMessage, Unit]): ServerIntent = + new ServerIntent(onRequest, PartialFunction.empty) + + def notify(onNotification: PartialFunction[JsonRpcNotificationMessage, Unit]): ServerIntent = + new ServerIntent(PartialFunction.empty, onNotification) +} + +/** + * Interface to invoke JSON-RPC response. + */ +trait ServerCallback { + def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit + def jsonRpcRespondError(execId: Option[String], code: Long, message: String): Unit + def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit + def appendExec(exec: Exec): Boolean + def log: Logger + def name: String + + private[sbt] def authOptions: Set[ServerAuthentication] + private[sbt] def authenticate(token: String): Boolean + private[sbt] def setInitialized(value: Boolean): Unit + private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit +} diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 55376c343..1bdee43ff 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -26,7 +26,7 @@ import sbt.internal.librarymanagement.mavenint.{ PomExtraDependencyAttributes, SbtPomExtraProperties } -import sbt.internal.server.{ LanguageServerReporter, Definition } +import sbt.internal.server.{ LanguageServerReporter, Definition, LanguageServerProtocol } import sbt.internal.testing.TestLogger import sbt.internal.util._ import sbt.internal.util.Attributed.data @@ -278,6 +278,12 @@ object Defaults extends BuildCommon { if (serverConnectionType.value == ConnectionType.Tcp) Set(ServerAuthentication.Token) else Set() }, + serverHandlers :== Nil, + fullServerHandlers := { + (Vector(LanguageServerProtocol.handler) + ++ serverHandlers.value + ++ Vector(ServerHandler.fallback)) + }, insideCI :== sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI"), )) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index d90311607..df551cdd7 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -136,6 +136,8 @@ object Keys { val serverHost = SettingKey(BasicKeys.serverHost) val serverAuthentication = SettingKey(BasicKeys.serverAuthentication) val serverConnectionType = SettingKey(BasicKeys.serverConnectionType) + val fullServerHandlers = SettingKey(BasicKeys.fullServerHandlers) + val serverHandlers = settingKey[Seq[ServerHandler]]("User-defined server handlers.") val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting) val watch = SettingKey(BasicKeys.watch) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index b3da0452a..38124e5a5 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -27,6 +27,7 @@ import Keys.{ serverPort, serverAuthentication, serverConnectionType, + fullServerHandlers, logLevel, watch } @@ -475,6 +476,7 @@ object Project extends ProjectExtra { val authentication: Option[Set[ServerAuthentication]] = get(serverAuthentication) val connectionType: Option[ConnectionType] = get(serverConnectionType) val srvLogLevel: Option[Level.Value] = (logLevel in (ref, serverLog)).get(structure.data) + val hs: Option[Seq[ServerHandler]] = get(fullServerHandlers) val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true)) val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand) @@ -491,6 +493,7 @@ object Project extends ProjectExtra { .put(templateResolverInfos.key, trs) .setCond(shellPrompt.key, prompt) .setCond(serverLogLevel, srvLogLevel) + .setCond(fullServerHandlers.key, hs) s.copy( attributes = newAttrs, definedCommands = newDefinedCommands diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 1daab4db2..2a4e1c105 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -20,6 +20,7 @@ import BasicKeys.{ serverAuthentication, serverConnectionType, serverLogLevel, + fullServerHandlers, logLevel } import java.net.Socket @@ -102,6 +103,7 @@ private[sbt] final class CommandExchange { s.get(serverAuthentication).getOrElse(Set(ServerAuthentication.Token)) lazy val connectionType = s.get(serverConnectionType).getOrElse(ConnectionType.Tcp) lazy val level = s.get(serverLogLevel).orElse(s.get(logLevel)).getOrElse(Level.Warn) + lazy val handlers = s.get(fullServerHandlers).getOrElse(Nil) def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = { val name = newNetworkName @@ -114,7 +116,7 @@ private[sbt] final class CommandExchange { log } val channel = - new NetworkChannel(name, socket, Project structure s, auth, instance, logger) + new NetworkChannel(name, socket, Project structure s, auth, instance, handlers, logger) subscribe(channel) } if (server.isEmpty && firstInstance.get) { @@ -210,7 +212,7 @@ private[sbt] final class CommandExchange { // in case we have a better client that can utilize the knowledge. import sbt.internal.langserver.codec.JsonProtocol._ if (broadcastStringMessage || (entry.channelName contains c.name)) - c.langNotify("window/logMessage", params) + c.jsonRpcNotify("window/logMessage", params) } catch { case _: IOException => toDel += c } } case _ => diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 637bd97d0..761073591 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -21,10 +21,70 @@ import sbt.util.Logger private[sbt] case class LangServerError(code: Long, message: String) extends Throwable(message) +private[sbt] object LanguageServerProtocol { + lazy val internalJsonProtocol = new InitializeOptionFormats with sjsonnew.BasicJsonProtocol {} + + lazy val serverCapabilities: ServerCapabilities = { + ServerCapabilities(textDocumentSync = + TextDocumentSyncOptions(true, 0, false, false, SaveOptions(false)), + hoverProvider = false, + definitionProvider = true) + } + + lazy val handler: ServerHandler = ServerHandler({ + case callback: ServerCallback => + import callback._ + ServerIntent( + { + import sbt.internal.langserver.codec.JsonProtocol._ + import internalJsonProtocol._ + def json(r: JsonRpcRequestMessage) = + r.params.getOrElse( + throw LangServerError(ErrorCodes.InvalidParams, + s"param is expected on '${r.method}' method.")) + + { + case r: JsonRpcRequestMessage if r.method == "initialize" => + if (authOptions(ServerAuthentication.Token)) { + val param = Converter.fromJson[InitializeParams](json(r)).get + val optionJson = param.initializationOptions.getOrElse( + throw LangServerError(ErrorCodes.InvalidParams, + "initializationOptions is expected on 'initialize' param.")) + val opt = Converter.fromJson[InitializeOption](optionJson).get + val token = opt.token.getOrElse(sys.error("'token' is missing.")) + if (authenticate(token)) () + else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") + } else () + setInitialized(true) + appendExec(Exec(s"collectAnalyses", Some(r.id), Some(CommandSource(name)))) + jsonRpcRespond(InitializeResult(serverCapabilities), Option(r.id)) + + case r: JsonRpcRequestMessage if r.method == "textDocument/definition" => + import scala.concurrent.ExecutionContext.Implicits.global + Definition.lspDefinition(json(r), r.id, CommandSource(name), log) + () + case r: JsonRpcRequestMessage if r.method == "sbt/exec" => + val param = Converter.fromJson[SbtExecParams](json(r)).get + appendExec(Exec(param.commandLine, Some(r.id), Some(CommandSource(name)))) + () + case r: JsonRpcRequestMessage if r.method == "sbt/setting" => + import sbt.protocol.codec.JsonProtocol._ + val param = Converter.fromJson[Q](json(r)).get + onSettingQuery(Option(r.id), param) + } + }, { + case n: JsonRpcNotificationMessage if n.method == "textDocument/didSave" => + appendExec(Exec(";compile; collectAnalyses", None, Some(CommandSource(name)))) + () + } + ) + }) +} + /** * Implements Language Server Protocol . */ -private[sbt] trait LanguageServerProtocol extends CommandChannel { +private[sbt] trait LanguageServerProtocol extends CommandChannel { self => lazy val internalJsonProtocol = new InitializeOptionFormats with sjsonnew.BasicJsonProtocol {} @@ -34,54 +94,24 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { protected def log: Logger protected def onSettingQuery(execId: Option[String], req: Q): Unit - protected def onNotification(notification: JsonRpcNotificationMessage): Unit = { - log.debug(s"onNotification: $notification") - notification.method match { - case "textDocument/didSave" => - append(Exec(";compile; collectAnalyses", None, Some(CommandSource(name)))) - () - case u => log.debug(s"Unhandled notification received: $u") - } - } + protected lazy val callbackImpl: ServerCallback = new ServerCallback { + def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = + self.jsonRpcRespond(event, execId) - protected def onRequestMessage(request: JsonRpcRequestMessage): Unit = { - import sbt.internal.langserver.codec.JsonProtocol._ - import internalJsonProtocol._ - def json = - request.params.getOrElse( - throw LangServerError(ErrorCodes.InvalidParams, - s"param is expected on '${request.method}' method.")) - log.debug(s"onRequestMessage: $request") - request.method match { - case "initialize" => - if (authOptions(ServerAuthentication.Token)) { - val param = Converter.fromJson[InitializeParams](json).get - val optionJson = param.initializationOptions.getOrElse( - throw LangServerError(ErrorCodes.InvalidParams, - "initializationOptions is expected on 'initialize' param.")) - val opt = Converter.fromJson[InitializeOption](optionJson).get - val token = opt.token.getOrElse(sys.error("'token' is missing.")) - if (authenticate(token)) () - else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") - } else () - setInitialized(true) - append(Exec(s"collectAnalyses", Some(request.id), Some(CommandSource(name)))) - langRespond(InitializeResult(serverCapabilities), Option(request.id)) - case "textDocument/definition" => - import scala.concurrent.ExecutionContext.Implicits.global - Definition.lspDefinition(json, request.id, CommandSource(name), log) - () - case "sbt/exec" => - val param = Converter.fromJson[SbtExecParams](json).get - append(Exec(param.commandLine, Some(request.id), Some(CommandSource(name)))) - () - case "sbt/setting" => { - import sbt.protocol.codec.JsonProtocol._ - val param = Converter.fromJson[Q](json).get - onSettingQuery(Option(request.id), param) - } - case unhandledRequest => log.debug(s"Unhandled request received: $unhandledRequest") - } + def jsonRpcRespondError(execId: Option[String], code: Long, message: String): Unit = + self.jsonRpcRespondError(execId, code, message) + + def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = + self.jsonRpcNotify(method, params) + + def appendExec(exec: Exec): Boolean = self.append(exec) + def log: Logger = self.log + def name: String = self.name + private[sbt] def authOptions: Set[ServerAuthentication] = self.authOptions + private[sbt] def authenticate(token: String): Boolean = self.authenticate(token) + private[sbt] def setInitialized(value: Boolean): Unit = self.setInitialized(value) + private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit = + self.onSettingQuery(execId, req) } /** @@ -97,7 +127,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { // LanguageServerReporter sends PublishDiagnosticsParams case "sbt.internal.langserver.PublishDiagnosticsParams" => // val p = event.message.asInstanceOf[PublishDiagnosticsParams] - // langNotify("textDocument/publishDiagnostics", p) + // jsonRpcNotify("textDocument/publishDiagnostics", p) case "xsbti.Problem" => () // ignore case _ => @@ -109,7 +139,7 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { /** * Respond back to Language Server's client. */ - private[sbt] def langRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = { + private[sbt] def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = { val m = JsonRpcResponseMessage("2.0", execId, Option(Converter.toJson[A](event).get), None) val bytes = Serialization.serializeResponseMessage(m) @@ -119,7 +149,9 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { /** * Respond back to Language Server's client. */ - private[sbt] def langError(execId: Option[String], code: Long, message: String): Unit = { + private[sbt] def jsonRpcRespondError(execId: Option[String], + code: Long, + message: String): Unit = { val e = JsonRpcResponseError(code, message, None) val m = JsonRpcResponseMessage("2.0", execId, None, Option(e)) val bytes = Serialization.serializeResponseMessage(m) @@ -129,10 +161,10 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { /** * Respond back to Language Server's client. */ - private[sbt] def langError[A: JsonFormat](execId: Option[String], - code: Long, - message: String, - data: A): Unit = { + private[sbt] def jsonRpcRespondError[A: JsonFormat](execId: Option[String], + code: Long, + message: String, + data: A): Unit = { val e = JsonRpcResponseError(code, message, Option(Converter.toJson[A](data).get)) val m = JsonRpcResponseMessage("2.0", execId, None, Option(e)) val bytes = Serialization.serializeResponseMessage(m) @@ -142,26 +174,19 @@ private[sbt] trait LanguageServerProtocol extends CommandChannel { /** * Notify to Language Server's client. */ - private[sbt] def langNotify[A: JsonFormat](method: String, params: A): Unit = { + private[sbt] def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = { val m = JsonRpcNotificationMessage("2.0", method, Option(Converter.toJson[A](params).get)) - log.debug(s"langNotify: $m") + log.debug(s"jsonRpcNotify: $m") val bytes = Serialization.serializeNotificationMessage(m) publishBytes(bytes) } def logMessage(level: String, message: String): Unit = { import sbt.internal.langserver.codec.JsonProtocol._ - langNotify( + jsonRpcNotify( "window/logMessage", LogMessageParams(MessageType.fromLevelString(level), message) ) } - - private[sbt] lazy val serverCapabilities: ServerCapabilities = { - ServerCapabilities(textDocumentSync = - TextDocumentSyncOptions(true, 0, false, false, SaveOptions(false)), - hoverProvider = false, - definitionProvider = true) - } } diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 3ad7f2fc5..e1dc9fac0 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -26,6 +26,7 @@ final class NetworkChannel(val name: String, structure: BuildStructure, auth: Set[ServerAuthentication], instance: ServerInstance, + handlers: Seq[ServerHandler], val log: Logger) extends CommandChannel with LanguageServerProtocol { @@ -76,7 +77,6 @@ final class NetworkChannel(val name: String, // contentType = "" state = SingleLine } - def tillEndOfLine: Option[Vector[Byte]] = { val delimPos = buffer.indexOf(delimiter) if (delimPos > 0) { @@ -165,6 +165,21 @@ final class NetworkChannel(val name: String, } } + private lazy val intents = { + val cb = callbackImpl + handlers.toVector map { h => + h.handler(cb) + } + } + lazy val onRequestMessage: PartialFunction[JsonRpcRequestMessage, Unit] = + intents.foldLeft(PartialFunction.empty[JsonRpcRequestMessage, Unit]) { + case (f, i) => f orElse i.onRequest + } + lazy val onNotification: PartialFunction[JsonRpcNotificationMessage, Unit] = + intents.foldLeft(PartialFunction.empty[JsonRpcNotificationMessage, Unit]) { + case (f, i) => f orElse i.onNotification + } + def handleBody(chunk: Vector[Byte]): Unit = { if (isLanguageServerProtocol) { Serialization.deserializeJsonMessage(chunk) match { @@ -174,7 +189,7 @@ final class NetworkChannel(val name: String, } catch { case LangServerError(code, message) => log.debug(s"sending error: $code: $message") - langError(Option(req.id), code, message) + jsonRpcRespondError(Option(req.id), code, message) } case Right(ntf: JsonRpcNotificationMessage) => try { @@ -182,13 +197,13 @@ final class NetworkChannel(val name: String, } catch { case LangServerError(code, message) => log.debug(s"sending error: $code: $message") - langError(None, code, message) // new id? + jsonRpcRespondError(None, code, message) // new id? } case Right(msg) => log.debug(s"Unhandled message: $msg") case Left(errorDesc) => val msg = s"Got invalid chunk from client (${new String(chunk.toArray, "UTF-8")}): " + errorDesc - langError(None, ErrorCodes.ParseError, msg) + jsonRpcRespondError(None, ErrorCodes.ParseError, msg) } } else { contentType match { @@ -230,7 +245,7 @@ final class NetworkChannel(val name: String, private[sbt] def notifyEvent[A: JsonFormat](method: String, params: A): Unit = { if (isLanguageServerProtocol) { - langNotify(method, params) + jsonRpcNotify(method, params) } else { () } @@ -240,7 +255,7 @@ final class NetworkChannel(val name: String, if (isLanguageServerProtocol) { event match { case entry: StringEvent => logMessage(entry.level, entry.message) - case _ => langRespond(event, execId) + case _ => jsonRpcRespond(event, execId) } } else { contentType match { @@ -341,8 +356,8 @@ final class NetworkChannel(val name: String, if (initialized) { import sbt.protocol.codec.JsonProtocol._ SettingQuery.handleSettingQueryEither(req, structure) match { - case Right(x) => langRespond(x, execId) - case Left(s) => langError(execId, ErrorCodes.InvalidParams, s) + case Right(x) => jsonRpcRespond(x, execId) + case Left(s) => jsonRpcRespondError(execId, ErrorCodes.InvalidParams, s) } } else { log.warn(s"ignoring query $req before initialization") diff --git a/sbt/src/server-test/handshake/build.sbt b/sbt/src/server-test/handshake/build.sbt index 192730eef..1267ea46b 100644 --- a/sbt/src/server-test/handshake/build.sbt +++ b/sbt/src/server-test/handshake/build.sbt @@ -1,6 +1,21 @@ lazy val root = (project in file(".")) .settings( Global / serverLog / logLevel := Level.Debug, + + // custom handler + Global / serverHandlers += ServerHandler({ callback => + import callback._ + import sjsonnew.BasicJsonProtocol._ + import sbt.internal.protocol.JsonRpcRequestMessage + ServerIntent( + { + case r: JsonRpcRequestMessage if r.method == "lunar/helo" => + jsonRpcNotify("lunar/oleh", "") + () + }, + PartialFunction.empty + ) + }), name := "handshake", scalaVersion := "2.12.3", ) diff --git a/sbt/src/test/scala/sbt/ServerSpec.scala b/sbt/src/test/scala/sbt/ServerSpec.scala index 7ad307fd8..5c25d1afb 100644 --- a/sbt/src/test/scala/sbt/ServerSpec.scala +++ b/sbt/src/test/scala/sbt/ServerSpec.scala @@ -20,15 +20,19 @@ class ServerSpec extends AsyncFlatSpec with Matchers { "server" should "start" in { withBuildSocket("handshake") { (out, in, tkn) => writeLine( - """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""", + """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "handshake/name" } }""", out) Thread.sleep(100) - val l2 = contentLength(in) - println(l2) - readLine(in) - readLine(in) - val x2 = readContentLength(in, l2) - println(x2) + + println(readFrame(in)) + println(readFrame(in)) + println(readFrame(in)) + println(readFrame(in)) + // println(readFrame(in)) + + writeLine("""{ "jsonrpc": "2.0", "id": 10, "method": "lunar/helo", "params": {} }""", out) + Thread.sleep(100) + assert(1 == 1) } } @@ -90,6 +94,14 @@ object ServerSpec { writeLine(message, out) } + def readFrame(in: InputStream): Option[String] = { + val l = contentLength(in) + println(l) + readLine(in) + readLine(in) + readContentLength(in, l) + } + def contentLength(in: InputStream): Int = { readLine(in) map { line => line.drop(16).toInt @@ -98,7 +110,11 @@ object ServerSpec { def readLine(in: InputStream): Option[String] = { if (buffer.isEmpty) { - val bytesRead = in.read(readBuffer) + val bytesRead = try { + in.read(readBuffer) + } catch { + case _: java.io.IOException => 0 + } if (bytesRead > 0) { buffer = buffer ++ readBuffer.toVector.take(bytesRead) } @@ -153,11 +169,12 @@ object ServerSpec { else { if (n <= 0) sys.error(s"Timeout. $portfile is not found.") else { + println(s" waiting for $portfile...") Thread.sleep(1000) waitForPortfile(n - 1) } } - waitForPortfile(10) + waitForPortfile(20) val (sk, tkn) = ClientSocket.socket(portfile) val out = sk.getOutputStream val in = sk.getInputStream @@ -172,7 +189,7 @@ object ServerSpec { sendJsonRpc( """{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }""", out) - shutdown() + // shutdown() } } } From f13465246c382d5416acf732da2bfee2ed68b28c Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 9 Mar 2018 18:01:10 -0500 Subject: [PATCH 2/3] include the full body in debug message --- main-command/src/main/scala/sbt/ServerHandler.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main-command/src/main/scala/sbt/ServerHandler.scala b/main-command/src/main/scala/sbt/ServerHandler.scala index 07fe8616e..ce9429c33 100644 --- a/main-command/src/main/scala/sbt/ServerHandler.scala +++ b/main-command/src/main/scala/sbt/ServerHandler.scala @@ -26,8 +26,8 @@ object ServerHandler { lazy val fallback: ServerHandler = ServerHandler({ handler => ServerIntent( - { case x => handler.log.debug(s"Unhandled notification received: ${x.method}") }, - { case x => handler.log.debug(s"Unhandled request received: ${x.method}") } + { case x => handler.log.debug(s"Unhandled notification received: ${x.method}: $x") }, + { case x => handler.log.debug(s"Unhandled request received: ${x.method}: $x") } ) }) } From 0433440c59c735d2ce908aaa300461759f5d914d Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 13 Mar 2018 23:42:19 +0900 Subject: [PATCH 3/3] move ServerHandler to internal per review --- main-command/src/main/scala/sbt/BasicKeys.scala | 1 + .../scala/sbt/{ => internal/server}/ServerHandler.scala | 2 ++ main/src/main/scala/sbt/Defaults.scala | 7 ++++++- main/src/main/scala/sbt/Keys.scala | 1 + main/src/main/scala/sbt/Project.scala | 1 + sbt/src/server-test/handshake/build.sbt | 2 ++ 6 files changed, 13 insertions(+), 1 deletion(-) rename main-command/src/main/scala/sbt/{ => internal/server}/ServerHandler.scala (98%) diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index 393397f3f..5ceeec21b 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -10,6 +10,7 @@ package sbt import java.io.File import sbt.internal.util.AttributeKey import sbt.internal.inc.classpath.ClassLoaderCache +import sbt.internal.server.ServerHandler import sbt.librarymanagement.ModuleID import sbt.util.Level diff --git a/main-command/src/main/scala/sbt/ServerHandler.scala b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala similarity index 98% rename from main-command/src/main/scala/sbt/ServerHandler.scala rename to main-command/src/main/scala/sbt/internal/server/ServerHandler.scala index ce9429c33..47b4f4fbf 100644 --- a/main-command/src/main/scala/sbt/ServerHandler.scala +++ b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala @@ -6,6 +6,8 @@ */ package sbt +package internal +package server import sjsonnew.JsonFormat import sbt.internal.protocol._ diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 1bdee43ff..36299ac25 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -26,7 +26,12 @@ import sbt.internal.librarymanagement.mavenint.{ PomExtraDependencyAttributes, SbtPomExtraProperties } -import sbt.internal.server.{ LanguageServerReporter, Definition, LanguageServerProtocol } +import sbt.internal.server.{ + LanguageServerReporter, + Definition, + LanguageServerProtocol, + ServerHandler +} import sbt.internal.testing.TestLogger import sbt.internal.util._ import sbt.internal.util.Attributed.data diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index df551cdd7..82191f556 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -42,6 +42,7 @@ import sbt.internal.{ } import sbt.io.{ FileFilter, WatchService } import sbt.internal.io.WatchState +import sbt.internal.server.ServerHandler import sbt.internal.util.{ AttributeKey, SourcePosition } import sbt.librarymanagement.Configurations.CompilerPlugin diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 38124e5a5..ea3a6b942 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -45,6 +45,7 @@ import sbt.internal.{ import sbt.internal.util.{ AttributeKey, AttributeMap, Dag, Relation, Settings, ~> } import sbt.internal.util.Types.{ const, idFun } import sbt.internal.util.complete.DefaultParsers +import sbt.internal.server.ServerHandler import sbt.librarymanagement.Configuration import sbt.util.{ Show, Level } import sjsonnew.JsonFormat diff --git a/sbt/src/server-test/handshake/build.sbt b/sbt/src/server-test/handshake/build.sbt index 1267ea46b..fcfe026ac 100644 --- a/sbt/src/server-test/handshake/build.sbt +++ b/sbt/src/server-test/handshake/build.sbt @@ -1,3 +1,5 @@ +import sbt.internal.ServerHandler + lazy val root = (project in file(".")) .settings( Global / serverLog / logLevel := Level.Debug,