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
This commit is contained in:
Eugene Yokota 2017-09-17 19:08:45 -04:00
parent 6b8e716428
commit 9d40404915
11 changed files with 219 additions and 11 deletions

View File

@ -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,

View File

@ -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))
}
}
}

View File

@ -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)
}

View File

@ -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(_)) =>

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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!
}

View File

@ -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)

View File

@ -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""")