start an instance of sbt in the background

This commit is contained in:
Eugene Yokota 2018-01-24 03:56:42 -05:00
parent a2347332ab
commit bd0e44c292
10 changed files with 237 additions and 131 deletions

View File

@ -20,4 +20,4 @@ install:
- SET PATH=C:\sbt\sbt\bin;%PATH%
- SET SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g -Dfile.encoding=UTF8
test_script:
- sbt "scripted actions/* server/*"
- sbt "scripted actions/*" "testOnly sbt.ServerSpec"

View File

@ -26,7 +26,7 @@ env:
- SBT_CMD="scripted dependency-management/*4of4"
- SBT_CMD="scripted java/* package/* reporter/* run/* project-load/*"
- SBT_CMD="scripted project/*1of2"
- SBT_CMD="scripted project/*2of2 server/*"
- SBT_CMD="scripted project/*2of2"
- SBT_CMD="scripted source-dependencies/*1of3"
- SBT_CMD="scripted source-dependencies/*2of3"
- SBT_CMD="scripted source-dependencies/*3of3"

View File

@ -294,6 +294,7 @@ lazy val actionsProj = (project in file("main-actions"))
lazy val protocolProj = (project in file("protocol"))
.enablePlugins(ContrabandPlugin, JsonCodecPlugin)
.dependsOn(collectionProj)
.settings(
testedBaseSettings,
name := "Protocol",
@ -441,7 +442,7 @@ lazy val sbtProj = (project in file("sbt"))
.dependsOn(mainProj, scriptedSbtProj % "test->test")
.enablePlugins(BuildInfoPlugin)
.settings(
baseSettings,
testedBaseSettings,
name := "sbt",
normalizedName := "sbt",
crossScalaVersions := Seq(baseScalaVersion),
@ -453,6 +454,8 @@ lazy val sbtProj = (project in file("sbt"))
buildInfoObject in Test := "TestBuildInfo",
buildInfoKeys in Test := Seq[BuildInfoKey](fullClasspath in Compile),
connectInput in run in Test := true,
outputStrategy in run in Test := Some(StdoutOutput),
fork in Test := true,
)
.configure(addSbtCompilerBridge)

View File

@ -0,0 +1,45 @@
/*
* sbt
* Copyright 2011 - 2017, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under BSD-3-Clause license (see LICENSE)
*/
package sbt
package protocol
import java.io.File
import java.net.{ Socket, URI, InetAddress }
import sjsonnew.BasicJsonProtocol
import sjsonnew.support.scalajson.unsafe.{ Parser, Converter }
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
import sbt.internal.protocol.{ PortFile, TokenFile }
import sbt.internal.protocol.codec.{ PortFileFormats, TokenFileFormats }
import sbt.internal.util.Util.isWindows
import org.scalasbt.ipcsocket._
object ClientSocket {
private lazy val fileFormats = new BasicJsonProtocol with PortFileFormats with TokenFileFormats {}
def socket(portfile: File): (Socket, Option[String]) = {
import fileFormats._
val json: JValue = Parser.parseFromFile(portfile).get
val p = Converter.fromJson[PortFile](json).get
val uri = new URI(p.uri)
// println(uri)
val token = p.tokenfilePath map { tp =>
val tokeFile = new File(tp)
val json: JValue = Parser.parseFromFile(tokeFile).get
val t = Converter.fromJson[TokenFile](json).get
t.token
}
val sk = uri.getScheme match {
case "local" if isWindows =>
new Win32NamedPipeSocket("""\\.\pipe\""" + uri.getSchemeSpecificPart)
case "local" => new UnixDomainSocket(uri.getSchemeSpecificPart)
case "tcp" => new Socket(InetAddress.getByName(uri.getHost), uri.getPort)
case _ => sys.error(s"Unsupported uri: $uri")
}
(sk, token)
}
}

View File

@ -1,106 +0,0 @@
package example
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 delimiter: Byte = '\n'.toByte
lazy val connection = getConnection
lazy val out = connection.getOutputStream
lazy val in = connection.getInputStream
val t = getToken
val msg0 = s"""{ "type": "InitCommand", "token": "$t" }"""
writeLine(s"Content-Length: ${ msg0.size + 2 }")
writeLine("Content-Type: application/sbt-x1")
writeLine("")
writeLine(msg0)
out.flush
writeLine("Content-Length: 49")
writeLine("Content-Type: application/sbt-x1")
writeLine("")
// 12345678901234567890123456789012345678901234567890
writeLine("""{ "type": "ExecCommand", "commandLine": "exit" }""")
writeLine("")
out.flush
val baseDirectory = new File(args(0))
IO.write(baseDirectory / "ok.txt", "ok")
def getToken: String = {
val tokenfile = new File(getTokenFileUri)
val json: JValue = Parser.parseFromFile(tokenfile).get
json match {
case JObject(fields) =>
(fields find { _.field == "token" } map { _.value }) match {
case Some(JString(value)) => value
case _ =>
sys.error("json doesn't token field that is JString")
}
case _ => sys.error("json doesn't have token field")
}
}
def getTokenFileUri: URI = {
val portfile = baseDirectory / "project" / "target" / "active.json"
val json: JValue = Parser.parseFromFile(portfile).get
json match {
case JObject(fields) =>
(fields find { _.field == "tokenfileUri" } map { _.value }) match {
case Some(JString(value)) => new URI(value)
case _ =>
sys.error("json doesn't tokenfile field that is JString")
}
case _ => sys.error("json doesn't have tokenfile field")
}
}
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 == "uri" } map { _.value }) match {
case Some(JString(value)) =>
val u = new URI(value)
u.getPort
case _ =>
sys.error("json doesn't uri field that is JString")
}
case _ => sys.error("json doesn't have uri field")
}
}
def getConnection: Socket =
try {
new Socket(InetAddress.getByName(host), getPort)
} catch {
case _ =>
Thread.sleep(1000)
getConnection
}
def writeLine(s: String): Unit = {
if (s != "") {
out.write(s.getBytes("UTF-8"))
}
writeEndLine
}
def writeEndLine(): Unit = {
val retByte: Byte = '\r'.toByte
val delimiter: Byte = '\n'.toByte
out.write(retByte.toInt)
out.write(delimiter.toInt)
out.flush
}
}

View File

@ -1,15 +0,0 @@
lazy val runClient = taskKey[Unit]("")
lazy val root = (project in file("."))
.settings(
serverConnectionType in Global := ConnectionType.Tcp,
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""")
}).value
)

View File

@ -1,6 +0,0 @@
> show serverPort
> runClient
-> shell
$ exists ok.txt

View File

@ -0,0 +1,6 @@
lazy val root = (project in file("."))
.settings(
Global / serverLog / logLevel := Level.Debug,
name := "handshake",
scalaVersion := "2.12.3",
)

View File

@ -22,7 +22,7 @@ object RunFromSourceMain {
// this arrangement is because Scala does not always properly optimize away
// the tail recursion in a catch statement
@tailrec private def run(baseDir: File, args: Seq[String]): Unit =
@tailrec private[sbt] def run(baseDir: File, args: Seq[String]): Unit =
runImpl(baseDir, args) match {
case Some((baseDir, args)) => run(baseDir, args)
case None => ()

View File

@ -0,0 +1,179 @@
/*
* sbt
* Copyright 2011 - 2017, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under BSD-3-Clause license (see LICENSE)
*/
package sbt
import org.scalatest._
import scala.concurrent._
import java.io.{ InputStream, OutputStream }
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.{ ThreadFactory, ThreadPoolExecutor }
import sbt.protocol.ClientSocket
class ServerSpec extends AsyncFlatSpec with Matchers {
import ServerSpec._
"server" should "start" in {
withBuildSocket("handshake") { (out, in, tkn) =>
writeLine(
"""{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""",
out)
Thread.sleep(100)
val l2 = contentLength(in)
println(l2)
readLine(in)
readLine(in)
val x2 = readContentLength(in, l2)
println(x2)
assert(1 == 1)
}
}
}
object ServerSpec {
private val serverTestBase: File = new File(".").getAbsoluteFile / "sbt" / "src" / "server-test"
private val nextThreadId = new AtomicInteger(1)
private val threadGroup = Thread.currentThread.getThreadGroup()
val readBuffer = new Array[Byte](4096)
var buffer: Vector[Byte] = Vector.empty
var bytesRead = 0
private val delimiter: Byte = '\n'.toByte
private val RetByte = '\r'.toByte
private val threadFactory = new ThreadFactory() {
override def newThread(runnable: Runnable): Thread = {
val thread =
new Thread(threadGroup,
runnable,
s"sbt-test-server-threads-${nextThreadId.getAndIncrement}")
// Do NOT setDaemon because then the code in TaskExit.scala in sbt will insta-kill
// the backgrounded process, at least for the case of the run task.
thread
}
}
private val executor = new ThreadPoolExecutor(
0, /* corePoolSize */
1, /* maxPoolSize, max # of servers */
2,
java.util.concurrent.TimeUnit.SECONDS,
/* keep alive unused threads this long (if corePoolSize < maxPoolSize) */
new java.util.concurrent.SynchronousQueue[Runnable](),
threadFactory
)
def backgroundRun(baseDir: File, args: Seq[String]): Unit = {
executor.execute(new Runnable {
def run(): Unit = {
RunFromSourceMain.run(baseDir, args)
}
})
}
def shutdown(): Unit = executor.shutdown()
def withBuildSocket(testBuild: String)(
f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = {
IO.withTemporaryDirectory { temp =>
IO.copyDirectory(serverTestBase / testBuild, temp / testBuild)
withBuildSocket(temp / testBuild)(f)
}
}
def sendJsonRpc(message: String, out: OutputStream): Unit = {
writeLine(s"""Content-Length: ${message.size + 2}""", out)
writeLine("", out)
writeLine(message, out)
}
def contentLength(in: InputStream): Int = {
readLine(in) map { line =>
line.drop(16).toInt
} getOrElse (0)
}
def readLine(in: InputStream): Option[String] = {
if (buffer.isEmpty) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
}
}
val delimPos = buffer.indexOf(delimiter)
if (delimPos > 0) {
val chunk0 = buffer.take(delimPos)
buffer = buffer.drop(delimPos + 1)
// remove \r at the end of line.
if (chunk0.size > 0 && chunk0.indexOf(RetByte) == chunk0.size - 1)
Some(new String(chunk0.dropRight(1).toArray, "utf-8"))
else Some(new String(chunk0.toArray, "utf-8"))
} else None // no EOL yet, so skip this turn.
}
def readContentLength(in: InputStream, length: Int): Option[String] = {
if (buffer.isEmpty) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
}
}
if (length <= buffer.size) {
val chunk = buffer.take(length)
buffer = buffer.drop(length)
Some(new String(chunk.toArray, "utf-8"))
} else None // have not read enough yet, so skip this turn.
}
def writeLine(s: String, out: OutputStream): Unit = {
def writeEndLine(): Unit = {
val retByte: Byte = '\r'.toByte
val delimiter: Byte = '\n'.toByte
out.write(retByte.toInt)
out.write(delimiter.toInt)
out.flush
}
if (s != "") {
out.write(s.getBytes("UTF-8"))
}
writeEndLine
}
def withBuildSocket(baseDirectory: File)(
f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = {
backgroundRun(baseDirectory, Nil)
val portfile = baseDirectory / "project" / "target" / "active.json"
def waitForPortfile(n: Int): Unit =
if (portfile.exists) ()
else {
if (n <= 0) sys.error(s"Timeout. $portfile is not found.")
else {
Thread.sleep(1000)
waitForPortfile(n - 1)
}
}
waitForPortfile(10)
val (sk, tkn) = ClientSocket.socket(portfile)
val out = sk.getOutputStream
val in = sk.getInputStream
sendJsonRpc(
"""{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { } } }""",
out)
try {
f(out, in, tkn)
} finally {
sendJsonRpc(
"""{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }""",
out)
shutdown()
}
}
}