Improve network client server forking

In the previous version of the NetworkClient, there was no feedback
while the client was starting up. It was also possible that if the
server had exited abruptly and there was a dead active.json portfile
left over, that the client wouldn't be able to start the server.

This commit reworks things so that we launch the server with a java
process and we print out the stdout, stderr streams from the process. We
also forward the client's stdin in case the server couldn't be started
and the user wants to retry or print the stacktrace.
This commit is contained in:
Ethan Atkins 2020-06-24 18:32:55 -07:00
parent 734a1e7641
commit f0815edc7a
1 changed files with 98 additions and 51 deletions

View File

@ -10,21 +10,24 @@ package internal
package client package client
import java.io.{ File, IOException, InputStream, PrintStream } import java.io.{ File, IOException, InputStream, PrintStream }
import java.lang.ProcessBuilder.Redirect
import java.net.Socket
import java.nio.file.Files
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams } import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams }
import sbt.internal.protocol._ import sbt.internal.protocol._
import sbt.internal.util.{ ConsoleAppender, ConsoleOut, LineReader } import sbt.internal.util.{ ConsoleAppender, ConsoleOut, LineReader, Terminal, Util }
import sbt.io.IO import sbt.io.IO
import sbt.io.syntax._ import sbt.io.syntax._
import sbt.protocol._ import sbt.protocol._
import sbt.util.Level import sbt.util.Level
import sjsonnew.support.scalajson.unsafe.Converter import sjsonnew.support.scalajson.unsafe.Converter
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
import scala.collection.mutable import scala.collection.mutable
import scala.sys.process.{ BasicIO, Process, ProcessLogger }
import scala.util.Properties import scala.util.Properties
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.util.{ Failure, Success } import scala.util.{ Failure, Success }
@ -65,23 +68,34 @@ class NetworkClient(
private val status = new AtomicReference("Ready") private val status = new AtomicReference("Ready")
private val lock: AnyRef = new AnyRef {} private val lock: AnyRef = new AnyRef {}
private val running = new AtomicBoolean(true) private val running = new AtomicBoolean(true)
private val connectionHolder = new AtomicReference[ServerConnection]
private def mkSocket(file: File): (Socket, Option[String]) = ClientSocket.socket(file, useJNI)
private val pendingExecIds = ListBuffer.empty[String] private val pendingExecIds = ListBuffer.empty[String]
private def baseDirectory: File = arguments.baseDirectory private def portfile = arguments.baseDirectory / "project" / "target" / "active.json"
lazy val connection = init() def connection: ServerConnection = connectionHolder.synchronized {
connectionHolder.get match {
case null => init(true)
case c => c
}
}
start() private[this] val sbtProcess = new AtomicReference[Process](null)
private class ConnectionRefusedException(t: Throwable) extends Throwable(t)
// Open server connection based on the portfile // Open server connection based on the portfile
def init(): ServerConnection = { def init(retry: Boolean): ServerConnection =
val portfile = baseDirectory / "project" / "target" / "active.json" try {
if (!portfile.exists) { if (!portfile.exists) {
forkServer(portfile) forkServer(portfile, log = true)
} }
val (sk, tkn) = ClientSocket.socket(portfile) val (sk, tkn) =
try mkSocket(portfile)
catch { case e: IOException => throw new ConnectionRefusedException(e) }
val conn = new ServerConnection(sk) { val conn = new ServerConnection(sk) {
override def onNotification(msg: JsonRpcNotificationMessage): Unit = self.onNotification(msg) override def onNotification(msg: JsonRpcNotificationMessage): Unit =
self.onNotification(msg)
override def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg) override def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg)
override def onResponse(msg: JsonRpcResponseMessage): Unit = self.onResponse(msg) override def onResponse(msg: JsonRpcResponseMessage): Unit = self.onResponse(msg)
override def onShutdown(): Unit = { override def onShutdown(): Unit = {
@ -92,43 +106,76 @@ class NetworkClient(
val execId = UUID.randomUUID.toString val execId = UUID.randomUUID.toString
val initCommand = InitCommand(tkn, Option(execId), Some(true)) val initCommand = InitCommand(tkn, Option(execId), Some(true))
conn.sendString(Serialization.serializeCommandAsJsonMessage(initCommand)) conn.sendString(Serialization.serializeCommandAsJsonMessage(initCommand))
connectionHolder.set(conn)
conn conn
} catch {
case e: ConnectionRefusedException if retry =>
if (Files.deleteIfExists(portfile.toPath)) init(retry = false)
else throw e
} }
/** /**
* Forks another instance of sbt in the background. * Forks another instance of sbt in the background.
* This instance must be shutdown explicitly via `sbt -client shutdown` * This instance must be shutdown explicitly via `sbt -client shutdown`
*/ */
def forkServer(portfile: File): Unit = { def forkServer(portfile: File, log: Boolean): Unit = {
console.appendLog(Level.Info, "server was not detected. starting an instance") if (log) console.appendLog(Level.Info, "server was not detected. starting an instance")
val args = List[String]() val color =
val launchOpts = List("-Xms2048M", "-Xmx2048M", "-Xss2M") if (!arguments.sbtArguments.exists(_.startsWith("-Dsbt.color=")))
val launcherJarString = sys.props.get("java.class.path") match { s"-Dsbt.color=${Terminal.console.isColorEnabled}" :: Nil
case Some(cp) => else Nil
cp.split(File.pathSeparator) val superShell =
.toList if (!arguments.sbtArguments.exists(_.startsWith("-Dsbt.supershell=")))
.headOption s"-Dsbt.supershell=${Terminal.console.isColorEnabled}" :: Nil
.getOrElse(sys.error("launcher JAR classpath not found")) else Nil
case _ => sys.error("property java.class.path expected")
val args = color ++ superShell ++ arguments.sbtArguments
val cmd = arguments.sbtScript +: args
val process =
new ProcessBuilder(cmd: _*)
.directory(arguments.baseDirectory)
.redirectInput(Redirect.PIPE)
.start()
sbtProcess.set(process)
val hook = new Thread(() => Option(sbtProcess.get).foreach(_.destroyForcibly()))
Runtime.getRuntime.addShutdownHook(hook)
val stdout = process.getInputStream
val stderr = process.getErrorStream
val stdin = process.getOutputStream
@tailrec
def blockUntilStart(): Unit = {
val stop = try {
while (stdout.available > 0) {
val byte = stdout.read
printStream.write(byte)
} }
val cmd = "java" :: launchOpts ::: "-jar" :: launcherJarString :: args while (stderr.available > 0) {
// val cmd = "sbt" val byte = stderr.read
val io = BasicIO(false, ProcessLogger(_ => ())) errorStream.write(byte)
val _ = Process(cmd, baseDirectory).run(io) }
def waitForPortfile(n: Int): Unit = while (System.in.available > 0) {
if (portfile.exists) { val byte = System.in.read
console.appendLog(Level.Info, "server found") stdin.write(byte)
} else { }
if (n <= 0) sys.error(s"timeout. $portfile is not found.") false
} catch {
case _: IOException => true
}
Thread.sleep(10)
if (!portfile.exists && !stop) blockUntilStart()
else { else {
Thread.sleep(1000) stdin.close()
if ((n - 1) % 10 == 0) { stdout.close()
console.appendLog(Level.Info, "waiting for the server...") stderr.close()
} process.getOutputStream.close()
waitForPortfile(n - 1)
} }
} }
waitForPortfile(90)
try blockUntilStart()
catch { case t: Throwable => t.printStackTrace() } finally {
sbtProcess.set(null)
Util.ignoreResult(Runtime.getRuntime.removeShutdownHook(hook))
}
} }
/** Called on the response for a returning message. */ /** Called on the response for a returning message. */