Skip to content

Commit

Permalink
Ensure Bloop runs on JVM that satisfies requirements
Browse files Browse the repository at this point in the history
(rebase onto master)
  • Loading branch information
tpasternak committed Oct 26, 2021
1 parent d54e6b8 commit 474572f
Show file tree
Hide file tree
Showing 22 changed files with 324 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,42 @@ package scala.build.bloop
import ch.epfl.scala.bsp4j
import org.eclipse.lsp4j.jsonrpc

import java.io.IOException
import java.io.{File, IOException}
import java.net.{ConnectException, Socket}
import java.nio.file.{Files, Path}
import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.{Future => JFuture, ScheduledExecutorService, TimeoutException}

import scala.annotation.tailrec
import scala.build.bloop.bloop4j.BloopExtraBuildParams
import scala.build.blooprifle.internal.Constants
import scala.build.blooprifle.{BloopRifle, BloopRifleConfig, BloopRifleLogger, BspConnection}
import scala.build.blooprifle.{
BloopRifle,
BloopRifleConfig,
BloopRifleLogger,
BloopServerRuntimeInfo,
BspConnection
}
import scala.concurrent.Await
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._

trait BloopServer {
def server: BuildServer

def shutdown(): Unit

def jvmVersion: String

def bloopVersion: String
}

object BloopServer {

private case class BloopServerImpl(
server: BuildServer,
listeningFuture: JFuture[Void],
socket: Socket
socket: Socket,
jvmVersion: String,
bloopVersion: String
) extends BloopServer {
def shutdown(): Unit = {
// Close the jsonrpc thread listening to input messages
Expand All @@ -41,27 +52,52 @@ object BloopServer {
config: BloopRifleConfig,
startServerChecksPool: ScheduledExecutorService,
logger: BloopRifleLogger
): Unit = {

val isBloopRunning = BloopRifle.check(config, logger, startServerChecksPool)

logger.debug(
if (isBloopRunning) s"Bloop is running on ${config.host}:${config.port}"
else s"No bloop daemon found on ${config.host}:${config.port}"
)

if (!isBloopRunning) {
logger.debug("Starting bloop server")
val serverStartedFuture = BloopRifle.startServer(
): BloopServerRuntimeInfo = {
import VersionOps._
val workdir = new File(".").getCanonicalFile.toPath
val bloopInfo =
BloopRifle.getCurrentBloopVersion(config, logger, workdir, startServerChecksPool)
val isRunning = bloopInfo.isDefined

def startBloop(bloopVersion: String, bloopJava: String) = {
logger.debug(s"Starting bloop $bloopVersion")
val fut = BloopRifle.startServer(
config,
startServerChecksPool,
logger
logger,
bloopVersion,
bloopJava
)

Await.result(serverStartedFuture, Duration.Inf)
logger.debug("Bloop server started")
Await.result(fut, 30.seconds)
logger.debug(s"Bloop server v $bloopVersion started")
}

val expectedBloopVersion = bloopInfo.map(_.bloopVersion)
.map(maxVersion(_, config.retainedBloopVersion))
.getOrElse(config.retainedBloopVersion)
val expectedBloopJvm = bloopInfo.map(_.jvmVersion)
.map(maxVersion(_, config.minimumBloopJvm))
.getOrElse(config.minimumBloopJvm)
val bloopVersionIsOk = bloopInfo.exists(_.bloopVersion == expectedBloopVersion)
val bloopJvmIsOk = bloopInfo.exists(_.jvmVersion == expectedBloopJvm)
val isOk = bloopVersionIsOk && bloopJvmIsOk
lazy val bloopInfoJavaPath = bloopInfo.map(i => Paths.get(i.javaHome, "bin", "java").toString())

val expectedBloopJava =
if (bloopJvmIsOk) bloopInfoJavaPath.getOrElse(
throw new RuntimeException("Fatal error, could not infer bloop java path")
)
else config.javaPath

if (isRunning) {
logger.debug(s"Bloop restart required. Currently running $bloopInfo}")
BloopRifle.exit(config, workdir, logger)
startBloop(expectedBloopVersion, expectedBloopJava)
}
else if (!isOk)
startBloop(expectedBloopVersion, expectedBloopJava)
BloopRifle.getCurrentBloopVersion(config, logger, workdir, startServerChecksPool)
.getOrElse(throw new RuntimeException("Fatal error, could not spawn Bloop"))
}

private def connect(
Expand Down Expand Up @@ -99,9 +135,9 @@ object BloopServer {
logger: BloopRifleLogger,
period: FiniteDuration,
timeout: FiniteDuration
): (BspConnection, Socket) = {
): (BspConnection, Socket, BloopServerRuntimeInfo) = {

ensureBloopRunning(config, threads.startServerChecks, logger)
val bloopInfo = ensureBloopRunning(config, threads.startServerChecks, logger)

logger.debug("Opening BSP connection with bloop")
Files.createDirectories(workspace.resolve(".scala/.bloop"))
Expand All @@ -116,7 +152,7 @@ object BloopServer {

logger.debug(s"Connected to Bloop via BSP at ${conn.address}")

(conn, socket)
(conn, socket, bloopInfo)
}

def buildServer(
Expand All @@ -130,7 +166,8 @@ object BloopServer {
logger: BloopRifleLogger
): BloopServer = {

val (conn, socket) = bsp(config, workspace, threads, logger, config.period, config.timeout)
val (conn, socket, bloopInfo) =
bsp(config, workspace, threads, logger, config.period, config.timeout)

logger.debug(s"Connected to Bloop via BSP at ${conn.address}")

Expand Down Expand Up @@ -172,7 +209,7 @@ object BloopServer {
}

server.onBuildInitialized()
BloopServerImpl(server, f, socket)
BloopServerImpl(server, f, socket, bloopInfo.jvmVersion, bloopInfo.bloopVersion)
}

def withBuildServer[T](
Expand Down Expand Up @@ -206,5 +243,4 @@ object BloopServer {
}
// format: on
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package scala.build.bloop

object VersionOps {
implicit class VersionOps_(lhs: String) {
def toInt(s: String): Option[Int] =
try Some(Integer.parseInt(s))
catch {
case _: NumberFormatException => None
}

def isNewerThan(rhs: String): Boolean = {
def lhsInt: Int => Option[Int] = lhs.split("[-.]").map(toInt).map(_.getOrElse(-1)).toList.lift
def rhsInt: Int => Option[Int] = rhs.split("[-.]").map(toInt).map(_.getOrElse(-1)).toList.lift
(0 until Int.MaxValue)
.takeWhile(i => rhsInt(i).isDefined || lhsInt(i).isDefined)
.map(i => (lhsInt(i), rhsInt(i)))
.find { case (l, r) => (l != r) }
.map { case (l, r) => l.getOrElse(-1) > r.getOrElse(-1) }
.getOrElse(false)
}
}

def maxVersion(lhs: String, rhs: String) = if (lhs isNewerThan rhs) lhs else rhs
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
package scala.build.blooprifle

import java.io.{
ByteArrayOutputStream,
File,
FileInputStream,
FileOutputStream,
InputStream,
OutputStream
}
import java.io.{ByteArrayOutputStream, FileInputStream, FileOutputStream, InputStream, OutputStream}
import java.nio.file.Path
import java.util.concurrent.ScheduledExecutorService

Expand Down Expand Up @@ -37,14 +30,7 @@ object BloopRifle {
config.port,
logger
)
check() && {
!BloopRifle.shutdownBloopIfVersionIncompatible(
config,
logger,
new File(".").getCanonicalFile.toPath,
scheduler
)
}
check()
}

/** Starts a new bloop server.
Expand All @@ -58,15 +44,17 @@ object BloopRifle {
def startServer(
config: BloopRifleConfig,
scheduler: ScheduledExecutorService,
logger: BloopRifleLogger
logger: BloopRifleLogger,
version: String,
bloopJava: String
): Future[Unit] =
config.classPath() match {
config.classPath(version) match {
case Left(ex) => Future.failed(new Exception("Error getting Bloop class path", ex))
case Right(cp) =>
Operations.startServer(
config.host,
config.port,
config.javaPath,
bloopJava,
config.javaOpts,
cp.map(_.toPath),
scheduler,
Expand Down Expand Up @@ -188,53 +176,58 @@ object BloopRifle {

def nullInputStream() = new FileInputStream(Util.devNull)

def extractVersionFromBloopAbout(stdoutFromBloopAbout: String): Option[String] =
stdoutFromBloopAbout.split("\n").find(_.startsWith("bloop v")).map(
// Probably we should implement an endpoint in Bloop to get this
// information in better form. I'm not sure error here should be escalated or ignored.
private def extractVersionFromBloopAbout(stdoutFromBloopAbout: String)
: Option[BloopServerRuntimeInfo] = {
val bloopVersion = stdoutFromBloopAbout.split("\n").find(_.startsWith("bloop v")).map(
_.split(" ")(1).trim().drop(1)
)
).getOrElse(throw new RuntimeException("Could not extract bloop version"))
val bloopJvmLine = stdoutFromBloopAbout.split("\n")
.find(_.startsWith("Running on Java JDK"))
.getOrElse(throw new RuntimeException("Could not bloop jvm info line"))
val bloopJvmVersion = bloopJvmLine
.split(" ")(4).stripPrefix("v").stripPrefix("1.")
val javaHome =
bloopJvmLine.split(" ").drop(5).headOption.map(
_.trim.stripPrefix("(").stripSuffix(")")
).getOrElse(throw new RuntimeException("Could not extract bloop java home"))
Some(BloopServerRuntimeInfo(
bloopVersion = bloopVersion,
jvmVersion = bloopJvmVersion,
javaHome = javaHome
))
}

def getCurrentBloopVersion(
config: BloopRifleConfig,
logger: BloopRifleLogger,
workdir: Path,
scheduler: ScheduledExecutorService
) = {
val bufferedOStream = new ByteArrayOutputStream(100000)
Operations.about(
config.host,
config.port,
workdir,
nullInputStream(),
bufferedOStream,
nullOutputStream(),
logger,
scheduler
)
extractVersionFromBloopAbout(new String(bufferedOStream.toByteArray))
}
val isRunning = BloopRifle.check(config, logger, scheduler)

/** Sometimes we need some minimal requirements for Bloop version. This method kills Bloop if its
* version is unsupported.
* @returns
* true if the 'exit' command has actually been sent to Bloop
*/
def shutdownBloopIfVersionIncompatible(
config: BloopRifleConfig,
logger: BloopRifleLogger,
workDir: Path,
scheduler: ScheduledExecutorService
): Boolean = {
val currentBloopVersion = getCurrentBloopVersion(config, logger, workDir, scheduler)
val isOk = config.acceptBloopVersion.forall { f =>
currentBloopVersion.forall(f(_))
}
if (isOk)
logger.debug("No need to restart Bloop")
else {
logger.debug(s"Shutting down unsupported Bloop $currentBloopVersion.")
val retCode = exit(config, workDir, logger)
logger.debug(s"Bloop exit code: $retCode")
if (isRunning) {
val bufferedOStream = new ByteArrayOutputStream(100000)
Operations.about(
config.host,
config.port,
workdir,
nullInputStream(),
bufferedOStream,
nullOutputStream(),
logger,
scheduler
)
extractVersionFromBloopAbout(new String(bufferedOStream.toByteArray))
}
!isOk
else
None
}
}

case class BloopServerRuntimeInfo(
bloopVersion: String,
jvmVersion: String,
javaHome: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final case class BloopRifleConfig(
port: Int,
javaPath: String,
javaOpts: Seq[String],
classPath: () => Either[Throwable, Seq[File]],
classPath: String => Either[Throwable, Seq[File]],
bspSocketOrPort: Option[() => BspConnectionAddress],
bspStdin: Option[InputStream],
bspStdout: Option[OutputStream],
Expand All @@ -21,7 +21,8 @@ final case class BloopRifleConfig(
startCheckPeriod: FiniteDuration,
startCheckTimeout: FiniteDuration,
initTimeout: FiniteDuration,
acceptBloopVersion: Option[String => Boolean]
minimumBloopJvm: String,
retainedBloopVersion: String
)

object BloopRifleConfig {
Expand Down Expand Up @@ -95,7 +96,7 @@ object BloopRifleConfig {
}

def default(
bloopClassPath: () => Either[Throwable, Seq[File]]
bloopClassPath: String => Either[Throwable, Seq[File]]
): BloopRifleConfig =
BloopRifleConfig(
host = defaultHost,
Expand All @@ -112,6 +113,7 @@ object BloopRifleConfig {
startCheckPeriod = 100.millis,
startCheckTimeout = 1.minute,
initTimeout = 30.seconds,
acceptBloopVersion = None
minimumBloopJvm = "8",
retainedBloopVersion = Constants.bloopVersion
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ object Operations {
val nailgunClient = TcpClient(host, port)
val streams = Streams(in, out, err)

timeout(1.minute, scheduler, logger) {
timeout(30.seconds, scheduler, logger) {
nailgunClient.run(
"about",
Array.empty,
Expand Down
Loading

0 comments on commit 474572f

Please sign in to comment.