package xsbt
package boot
import java.io.File
import scala.util.control.NonFatal
import java.net.URI
import java.io.IOException
import Pre._
import scala.annotation.tailrec
class ServerApplication private (provider: xsbti.AppProvider) extends xsbti.AppMain {
import ServerApplication._
override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance
val server = serverMain.start(configuration)
System.out.println(s"${SERVER_SYNCH_TEXT}${server.uri}")
server.awaitTermination()
}
}
object ServerApplication {
val SERVER_SYNCH_TEXT = "[SERVER-URI]"
val ServerMainClass = classOf[xsbti.ServerMain]
def isServerApplication(clazz: Class[_]): Boolean =
ServerMainClass.isAssignableFrom(clazz)
def apply(provider: xsbti.AppProvider): xsbti.AppMain =
new ServerApplication(provider)
}
object ServerLocator {
private def locked[U](file: File)(f: => U): U = {
Locks(file, new java.util.concurrent.Callable[U] {
def call(): U = f
})
}
def makeLockFile(f: File): File =
new File(f.getParentFile, s"${f.getName}.lock")
def locate(currentDirectory: File, config: LaunchConfiguration): URI =
config.serverConfig match {
case None => sys.error("No server lock file configured. Cannot locate server.")
case Some(sc) => locked(makeLockFile(sc.lockFile)) {
readProperties(sc.lockFile) match {
case Some(uri) if isReachable(uri) => uri
case _ =>
val uri = ServerLauncher.startServer(currentDirectory, config)
writeProperties(sc.lockFile, uri)
uri
}
}
}
private val SERVER_URI_PROPERTY = "server.uri"
def readProperties(f: File): Option[java.net.URI] = {
try {
val props = Pre.readProperties(f)
props.getProperty(SERVER_URI_PROPERTY) match {
case null => None
case uri => Some(new java.net.URI(uri))
}
} catch {
case e: IOException => None
}
}
def writeProperties(f: File, uri: URI): Unit = {
val props = new java.util.Properties
props.setProperty(SERVER_URI_PROPERTY, uri.toASCIIString)
val output = new java.io.FileOutputStream(f)
val df = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ")
df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))
Pre.writeProperties(props, f, s"Server Startup at ${df.format(new java.util.Date)}")
}
def isReachable(uri: java.net.URI): Boolean =
try {
val socket = new java.net.Socket(uri.getHost, uri.getPort)
try socket.isConnected
finally socket.close()
} catch {
case e: IOException => false
}
}
class StreamDumper(in: java.io.BufferedReader, out: java.io.PrintStream) extends Thread {
setDaemon(true)
val endTime = new java.util.concurrent.atomic.AtomicLong(Long.MaxValue)
override def run(): Unit = {
def read(): Unit = if (endTime.get > System.currentTimeMillis) in.readLine match {
case null => ()
case line =>
out.println(line)
out.flush()
read()
}
read()
out.close()
}
def close(waitForErrors: Boolean): Unit = {
if (waitForErrors) {
endTime.set(System.currentTimeMillis + 5000)
Thread.`yield`()
while (isAlive() && (endTime.get > System.currentTimeMillis))
Thread.sleep(50)
} else {
endTime.set(System.currentTimeMillis)
}
}
}
object ServerLauncher {
import ServerApplication.SERVER_SYNCH_TEXT
def startServer(currentDirectory: File, config: LaunchConfiguration): URI = {
val serverConfig = config.serverConfig match {
case Some(c) => c
case None => throw new RuntimeException("Logic Failure: Attempting to start a server that isn't configured to be a server. Please report a bug.")
}
val launchConfig = java.io.File.createTempFile("sbtlaunch", "config")
if (System.getenv("SBT_SERVER_SAVE_TEMPS") eq null)
launchConfig.deleteOnExit()
LaunchConfiguration.save(config, launchConfig)
val jvmArgs: List[String] = serverJvmArgs(currentDirectory, serverConfig)
val cmd: List[String] =
("java" :: jvmArgs) ++
("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil)
launchProcessAndGetUri(cmd, currentDirectory)
}
def launchProcessAndGetUri(cmd: List[String], cwd: File): URI = {
val pb = new java.lang.ProcessBuilder()
pb.command(cmd: _*)
pb.directory(cwd)
val process = pb.start()
process.getOutputStream.close()
val stderr = process.getErrorStream
val stdout = process.getInputStream
val errorDumper = new StreamDumper(new java.io.BufferedReader(new java.io.InputStreamReader(stderr)), System.err)
errorDumper.start()
try readUntilSynch(new java.io.BufferedReader(new java.io.InputStreamReader(stdout))) match {
case Some(uri) => uri
case _ =>
try process.destroy() catch { case e: Exception => }
errorDumper.close(waitForErrors = true)
sys.error(s"Failed to start server process in ${pb.directory} command line ${pb.command}")
} finally {
errorDumper.close(waitForErrors = false)
stdout.close()
}
}
object ServerUriLine {
def unapply(in: String): Option[URI] =
if (in startsWith SERVER_SYNCH_TEXT) {
Some(new URI(in.substring(SERVER_SYNCH_TEXT.size)))
} else None
}
def readUntilSynch(in: java.io.BufferedReader): Option[URI] = {
@tailrec
def read(): Option[URI] = in.readLine match {
case null => None
case ServerUriLine(uri) => Some(uri)
case line => read()
}
try read()
finally in.close()
}
def readLines(f: File): List[String] =
if (!f.exists) Nil else {
val reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"))
@tailrec
def read(current: List[String]): List[String] =
reader.readLine match {
case null => current.reverse
case line => read(line :: current)
}
try read(Nil)
finally reader.close()
}
def javaIsAbove(currentDirectory: File, version: Int): Option[Boolean] = try {
val pb = new java.lang.ProcessBuilder()
pb.command("java", "-version")
pb.directory(currentDirectory)
val process = pb.start()
try {
process.getOutputStream.close()
process.getInputStream.close()
val stderr = new java.io.LineNumberReader(new java.io.InputStreamReader(process.getErrorStream))
val lineOption = try Option(stderr.readLine()) finally stderr.close()
val pattern = java.util.regex.Pattern.compile("""java version "[0-9]+\.([0-9]+)\..*".*""")
lineOption flatMap { line =>
val matcher = pattern.matcher(line)
if (matcher.matches()) {
try Some(Integer.parseInt(matcher.group(1)) > version) catch { case NonFatal(_) => None }
} else {
System.err.println(s"Failed to parse version from 'java -version' output '$line'")
None
}
}
} finally {
process.destroy()
try { process.waitFor() } catch { case NonFatal(_) => }
}
} catch {
case e: IOException =>
System.err.println(s"Failed to run 'java -version': ${e.getClass.getName}: ${e.getMessage}")
None
}
def serverJvmArgs(currentDirectory: File, serverConfig: ServerConfiguration): List[String] =
serverJvmArgs(currentDirectory, serverConfig.jvmArgs map readLines getOrElse Nil)
final val memOptPrefixes = List("-Xmx", "-Xms", "-XX:MaxPermSize", "-XX:PermSize", "-XX:ReservedCodeCacheSize", "-XX:MaxMetaspaceSize", "-XX:MetaspaceSize")
final val defaultMinHeapM = 256
final val defaultMaxHeapM = defaultMinHeapM * 4
final val defaultMinPermM = 64
final val defaultMaxPermM = defaultMinPermM * 4
def serverJvmArgs(currentDirectory: File, baseArgs: List[String]): List[String] = {
val trimmed = baseArgs.map(_.trim).filterNot(_.isEmpty)
def isMemoryOption(s: String) = memOptPrefixes.exists(s.startsWith(_))
if (trimmed.exists(isMemoryOption(_)))
trimmed
else {
val permOptions = javaIsAbove(currentDirectory, 7) match {
case Some(true) => List(s"-XX:MetaspaceSize=${defaultMinPermM}m", s"-XX:MaxMetaspaceSize=${defaultMaxPermM}m")
case Some(false) => List(s"-XX:PermSize=${defaultMinPermM}m", s"-XX:MaxPermSize=${defaultMaxPermM}m")
case None => Nil
}
s"-Xms${defaultMinHeapM}m" :: s"-Xmx${defaultMaxHeapM}m" :: (permOptions ++ trimmed)
}
}
def defaultLauncherLookup: File =
try {
val classInLauncher = classOf[AppConfiguration]
val fileOpt = for {
domain <- Option(classInLauncher.getProtectionDomain)
source <- Option(domain.getCodeSource)
location = source.getLocation
} yield toFile(location)
fileOpt.getOrElse(throw new RuntimeException("Could not inspect protection domain or code source"))
} catch {
case e: Throwable => throw new RuntimeException("Unable to find sbt-launch.jar.", e)
}
}