From 9d40404915f650579f787a88f1d2abc2c9ff17f2 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 17 Sep 2017 19:08:45 -0400 Subject: [PATCH] JSON port file This implements JSON-based port file. Thoughout the lifetime of the sbt server there will be `cwd / "project" / "target" / "active.json"`, which contains `url` field. Using this `url` the potential client, such as IDEs can find out which port number to hit. Ref #3508 --- build.sbt | 4 ++ .../scala/sbt/internal/server/Server.scala | 38 +++++++++++++--- main/src/main/scala/sbt/Main.scala | 4 +- .../scala/sbt/internal/CommandExchange.scala | 7 ++- .../sbt/internal/protocol/PortFile.scala | 45 +++++++++++++++++++ .../sbt/internal/protocol/TokenFile.scala | 36 +++++++++++++++ .../protocol/codec/PortFileFormats.scala | 29 ++++++++++++ .../protocol/codec/TokenFileFormats.scala | 29 ++++++++++++ protocol/src/main/contraband/portfile.contra | 16 +++++++ .../sbt-test/server/handshake/Client.scala | 21 ++++++++- sbt/src/sbt-test/server/handshake/build.sbt | 1 + 11 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/protocol/PortFile.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/protocol/TokenFile.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/protocol/codec/PortFileFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/protocol/codec/TokenFileFormats.scala create mode 100644 protocol/src/main/contraband/portfile.contra diff --git a/build.sbt b/build.sbt index 7e4e6d751..a9e37dd37 100644 --- a/build.sbt +++ b/build.sbt @@ -290,6 +290,10 @@ lazy val commandProj = (project in file("main-command")) sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats, mimaSettings, + mimaBinaryIssueFilters ++= Vector( + // Changed the signature of Server method. nacho cheese. + exclude[DirectMissingMethodProblem]("sbt.internal.server.Server.*") + ) ) .configure( addSbtIO, diff --git a/main-command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala index 7b764225f..45ea5ad67 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -5,12 +5,17 @@ package sbt package internal package server +import java.io.File import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, Socket } import java.util.concurrent.atomic.AtomicBoolean -import sbt.util.Logger -import sbt.internal.util.ErrorHandling import scala.concurrent.{ Future, Promise } import scala.util.{ Try, Success, Failure } +import sbt.internal.util.ErrorHandling +import sbt.internal.protocol.PortFile +import sbt.util.Logger +import sbt.io.IO +import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter } +import sbt.internal.protocol.codec._ private[sbt] sealed trait ServerInstance { def shutdown(): Unit @@ -18,14 +23,19 @@ private[sbt] sealed trait ServerInstance { } private[sbt] object Server { + sealed trait JsonProtocol + extends sjsonnew.BasicJsonProtocol + with PortFileFormats + with TokenFileFormats + object JsonProtocol extends JsonProtocol + def start(host: String, port: Int, onIncomingSocket: Socket => Unit, - /*onIncomingCommand: CommandMessage => Unit,*/ log: Logger): ServerInstance = + portfile: File, + tokenfile: File, + log: Logger): ServerInstance = new ServerInstance { - - // val lock = new AnyRef {} - // val clients: mutable.ListBuffer[ClientConnection] = mutable.ListBuffer.empty val running = new AtomicBoolean(false) val p: Promise[Unit] = Promise[Unit]() val ready: Future[Unit] = p.future @@ -41,6 +51,7 @@ private[sbt] object Server { case Success(serverSocket) => serverSocket.setSoTimeout(5000) log.info(s"sbt server started at $host:$port") + writePortfile() running.set(true) p.success(()) while (running.get()) { @@ -58,8 +69,21 @@ private[sbt] object Server { override def shutdown(): Unit = { log.info("shutting down server") + if (portfile.exists) { + IO.delete(portfile) + } + if (tokenfile.exists) { + IO.delete(tokenfile) + } running.set(false) } - } + // This file exists through the lifetime of the server. + def writePortfile(): Unit = { + import JsonProtocol._ + val p = PortFile(s"tcp://$host:$port", None) + val json = Converter.toJson(p).get + IO.write(portfile, CompactPrinter(json)) + } + } } diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index f4043f989..47a67a0fd 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -101,7 +101,9 @@ object StandardMain { val previous = TrapExit.installManager() try { try { - MainLoop.runLogged(s) + try { + MainLoop.runLogged(s) + } finally exchange.shutdown } finally DefaultBackgroundJobService.backgroundJobService.shutdown() } finally TrapExit.uninstallManager(previous) } diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index d56edb245..9cf8f6c62 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -15,6 +15,8 @@ import sjsonnew.JsonFormat import scala.concurrent.Await import scala.concurrent.duration.Duration import scala.util.{ Success, Failure } +import sbt.io.syntax._ +import sbt.io.Hash /** * The command exchange merges multiple command channels (e.g. network and console), @@ -87,7 +89,10 @@ private[sbt] final class CommandExchange { server match { case Some(x) => // do nothing case _ => - val x = Server.start("127.0.0.1", port, onIncomingSocket, s.log) + val portfile = (new File(".")).getAbsoluteFile / "project" / "target" / "active.json" + val h = Hash.halfHashString(portfile.toURL.toString) + val tokenfile = BuildPaths.getGlobalBase(s) / "server" / h / "token.json" + val x = Server.start("127.0.0.1", port, onIncomingSocket, portfile, tokenfile, s.log) Await.ready(x.ready, Duration("10s")) x.ready.value match { case Some(Success(_)) => diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/PortFile.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/PortFile.scala new file mode 100644 index 000000000..542ab368f --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/PortFile.scala @@ -0,0 +1,45 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.protocol +/** + * This file should exist throughout the lifetime of the server. + * It can be used to find out the transport protocol (port number etc). + */ +final class PortFile private ( + /** URL of the sbt server. */ + val url: String, + val tokenfile: Option[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: PortFile => (this.url == x.url) && (this.tokenfile == x.tokenfile) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.protocol.PortFile".##) + url.##) + tokenfile.##) + } + override def toString: String = { + "PortFile(" + url + ", " + tokenfile + ")" + } + protected[this] def copy(url: String = url, tokenfile: Option[String] = tokenfile): PortFile = { + new PortFile(url, tokenfile) + } + def withUrl(url: String): PortFile = { + copy(url = url) + } + def withTokenfile(tokenfile: Option[String]): PortFile = { + copy(tokenfile = tokenfile) + } + def withTokenfile(tokenfile: String): PortFile = { + copy(tokenfile = Option(tokenfile)) + } +} +object PortFile { + + def apply(url: String, tokenfile: Option[String]): PortFile = new PortFile(url, tokenfile) + def apply(url: String, tokenfile: String): PortFile = new PortFile(url, Option(tokenfile)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/TokenFile.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/TokenFile.scala new file mode 100644 index 000000000..12f1ac21c --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/TokenFile.scala @@ -0,0 +1,36 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.protocol +final class TokenFile private ( + val url: String, + val token: String) extends Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TokenFile => (this.url == x.url) && (this.token == x.token) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.protocol.TokenFile".##) + url.##) + token.##) + } + override def toString: String = { + "TokenFile(" + url + ", " + token + ")" + } + protected[this] def copy(url: String = url, token: String = token): TokenFile = { + new TokenFile(url, token) + } + def withUrl(url: String): TokenFile = { + copy(url = url) + } + def withToken(token: String): TokenFile = { + copy(token = token) + } +} +object TokenFile { + + def apply(url: String, token: String): TokenFile = new TokenFile(url, token) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/PortFileFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/PortFileFormats.scala new file mode 100644 index 000000000..daac10706 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/PortFileFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait PortFileFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val PortFileFormat: JsonFormat[sbt.internal.protocol.PortFile] = new JsonFormat[sbt.internal.protocol.PortFile] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.protocol.PortFile = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val url = unbuilder.readField[String]("url") + val tokenfile = unbuilder.readField[Option[String]]("tokenfile") + unbuilder.endObject() + sbt.internal.protocol.PortFile(url, tokenfile) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.protocol.PortFile, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("url", obj.url) + builder.addField("tokenfile", obj.tokenfile) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/TokenFileFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/TokenFileFormats.scala new file mode 100644 index 000000000..d812593b2 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/TokenFileFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TokenFileFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TokenFileFormat: JsonFormat[sbt.internal.protocol.TokenFile] = new JsonFormat[sbt.internal.protocol.TokenFile] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.protocol.TokenFile = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val url = unbuilder.readField[String]("url") + val token = unbuilder.readField[String]("token") + unbuilder.endObject() + sbt.internal.protocol.TokenFile(url, token) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.protocol.TokenFile, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("url", obj.url) + builder.addField("token", obj.token) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/portfile.contra b/protocol/src/main/contraband/portfile.contra new file mode 100644 index 000000000..f10d9c170 --- /dev/null +++ b/protocol/src/main/contraband/portfile.contra @@ -0,0 +1,16 @@ +package sbt.internal.protocol +@target(Scala) +@codecPackage("sbt.internal.protocol.codec") + +## This file should exist throughout the lifetime of the server. +## It can be used to find out the transport protocol (port number etc). +type PortFile { + ## URL of the sbt server. + url: String! + tokenfile: String +} + +type TokenFile { + url: String! + token: String! +} diff --git a/sbt/src/sbt-test/server/handshake/Client.scala b/sbt/src/sbt-test/server/handshake/Client.scala index ee0255abf..54b7097f3 100644 --- a/sbt/src/sbt-test/server/handshake/Client.scala +++ b/sbt/src/sbt-test/server/handshake/Client.scala @@ -4,10 +4,11 @@ import java.net.{ URI, Socket, InetAddress, SocketException } import sbt.io._ import sbt.io.syntax._ import java.io.File +import sjsonnew.support.scalajson.unsafe.{ Parser, Converter, CompactPrinter } +import sjsonnew.shaded.scalajson.ast.unsafe.{ JValue, JObject, JString } object Client extends App { val host = "127.0.0.1" - val port = 5123 val delimiter: Byte = '\n'.toByte println("hello") @@ -24,9 +25,25 @@ object Client extends App { val baseDirectory = new File(args(0)) IO.write(baseDirectory / "ok.txt", "ok") + def getPort: Int = { + val portfile = baseDirectory / "project" / "target" / "active.json" + val json: JValue = Parser.parseFromFile(portfile).get + json match { + case JObject(fields) => + (fields find { _.field == "url" } map { _.value }) match { + case Some(JString(value)) => + val u = new URI(value) + u.getPort + case _ => + sys.error("json doesn't url field that is JString") + } + case _ => sys.error("json doesn't have url field") + } + } + def getConnection: Socket = try { - new Socket(InetAddress.getByName(host), port) + new Socket(InetAddress.getByName(host), getPort) } catch { case _ => Thread.sleep(1000) diff --git a/sbt/src/sbt-test/server/handshake/build.sbt b/sbt/src/sbt-test/server/handshake/build.sbt index 9692ab267..fd924f0e4 100644 --- a/sbt/src/sbt-test/server/handshake/build.sbt +++ b/sbt/src/sbt-test/server/handshake/build.sbt @@ -5,6 +5,7 @@ lazy val root = (project in file(".")) scalaVersion := "2.12.3", serverPort in Global := 5123, libraryDependencies += "org.scala-sbt" %% "io" % "1.0.1", + libraryDependencies += "com.eed3si9n" %% "sjson-new-scalajson" % "0.8.0", runClient := (Def.taskDyn { val b = baseDirectory.value (bgRun in Compile).toTask(s""" $b""")