Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bloop jvm settings #3746

Merged
merged 8 commits into from
Apr 20, 2022
Merged
247 changes: 238 additions & 9 deletions metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ import java.io.PrintStream
import java.nio.channels.Channels
import java.nio.channels.Pipe
import java.nio.charset.StandardCharsets
import java.nio.file.Paths

import scala.concurrent.ExecutionContextExecutorService
import scala.concurrent.Future
import scala.concurrent.Promise
import scala.util.Failure
import scala.util.Success
import scala.util.Try

import scala.meta.internal.bsp.BuildChange
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.io.AbsolutePath

import bloop.bloopgun.BloopgunCli
import bloop.bloopgun.core.Shell
import bloop.launcher.LauncherMain
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.MessageActionItem
import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.Position

/**
* Establishes a connection with a bloop server using Bloop Launcher.
Expand All @@ -34,7 +41,7 @@ import org.eclipse.lsp4j.services.LanguageClient
*/
final class BloopServers(
client: MetalsBuildClient,
languageClient: LanguageClient,
languageClient: MetalsLanguageClient,
tables: Tables,
config: MetalsServerConfig
)(implicit ec: ExecutionContextExecutorService) {
Expand Down Expand Up @@ -78,17 +85,17 @@ final class BloopServers(
}

/**
* Ensure Bloop is running the inteded version that the user has passed
* Ensure Bloop is running the intended version that the user has passed
* in via UserConfiguration. If not, shut Bloop down and reconnect to it.
*
* @param expectedVersion desired version that the user has passed in. This
* could either be a newly passed in version from the
* user or the default Bloop version.
* @param runningVersion the current running version of Bloop.
* @param userDefinedNew whether or not the user has defined a new version.
* @param userDefinedOld whether or not the user has the version running
* defined or if they are just running the default.
* @param reconnect function to connect back to the build server.
* user or the default Bloop version.
* @param runningVersion the current running version of Bloop.
* @param userDefinedNew whether or not the user has defined a new version.
* @param userDefinedOld whether or not the user has the version running
* defined or if they are just running the default.
* @param reconnect function to connect back to the build server.
*/
def ensureDesiredVersion(
expectedVersion: String,
Expand Down Expand Up @@ -120,6 +127,226 @@ final class BloopServers(
}
}

private def writeJVMPropertiesToBloopGlobalJsonFile(
bloopGlobalJsonFilePath: AbsolutePath,
bloopCreatedByMetalsFilePath: AbsolutePath,
requestedBloopJvmProperties: List[String],
maybeJavaHome: Option[String]
): Try[Unit] = Try {

val javaOptionsString =
s"\"javaOptions\": [${requestedBloopJvmProperties.map(property => s"\"$property\"").mkString(", ")}]"

val jvmPropertiesString = maybeJavaHome
.map { javaHome =>
s"""|{
| $javaOptionsString,
| \"javaHome\": \"$javaHome\"
|}""".stripMargin
}
.getOrElse(s"{$javaOptionsString}")
bloopGlobalJsonFilePath.writeText(jvmPropertiesString)
bloopCreatedByMetalsFilePath.writeText(
bloopGlobalJsonFilePath.toNIO.toFile.lastModified().toString
)
}

private def getBloopGlobalJsonLastModifiedByMetalsTime(
bloopCreatedByMetalsFilePath: AbsolutePath
): Long = Try {
bloopCreatedByMetalsFilePath.readText.toLong
}.getOrElse(0)

private def processUserPreferenceForBloopJvmProperties(
messageActionItem: MessageActionItem,
bloopGlobalJsonFilePath: AbsolutePath,
bloopCreatedByMetalsFilePath: AbsolutePath,
requestedBloopJvmProperties: List[String],
maybeJavaHome: Option[String],
reconnect: () => Future[BuildChange]
): Future[Unit] = {
messageActionItem match {

case item
if item == Messages.BloopGlobalJsonFilePremodified.applyAndRestart =>
writeJVMPropertiesToBloopGlobalJsonFile(
bloopGlobalJsonFilePath,
bloopCreatedByMetalsFilePath,
requestedBloopJvmProperties,
maybeJavaHome
) match {
case Failure(exception) => Future.failed(exception)
case Success(_) =>
shutdownServer()
reconnect().ignoreValue
}

case item
if item == Messages.BloopGlobalJsonFilePremodified.openGlobalJsonFile =>
val position = new Position(0, 0)
val range = new org.eclipse.lsp4j.Range(position, position)
val command = ClientCommands.GotoLocation.toExecuteCommandParams(
ClientCommands.WindowLocation(
bloopGlobalJsonFilePath.toURI.toString,
range
)
)
Future.successful(languageClient.metalsExecuteClientCommand(command))

case item
if item == Messages.BloopGlobalJsonFilePremodified.useGlobalFile =>
Future.successful(())
}
}

private def updateBloopGlobalJsonFileThenRestart(
bloopGlobalJsonFilePath: AbsolutePath,
bloopCreatedByMetalsFilePath: AbsolutePath,
requestedBloopJvmProperties: List[String],
maybeJavaHome: Option[String],
zmerr marked this conversation as resolved.
Show resolved Hide resolved
reconnect: () => Future[BuildChange]
): Future[Unit] = {
writeJVMPropertiesToBloopGlobalJsonFile(
bloopGlobalJsonFilePath,
bloopCreatedByMetalsFilePath,
requestedBloopJvmProperties,
maybeJavaHome
) match {
case Failure(exception) => Future.failed(exception)
case Success(_) =>
languageClient
.showMessageRequest(
Messages.BloopJvmPropertiesChange.params()
)
.asScala
.flatMap {
case messageActionItem
if messageActionItem == Messages.BloopJvmPropertiesChange.reconnect =>
shutdownServer()
reconnect().ignoreValue
case _ =>
Future.successful(())
}
}

}

private def getBloopFilePath(fileName: String): Option[AbsolutePath] = {
Try {
AbsolutePath(
Paths
.get(System.getProperty("user.home"))
.resolve(s".bloop/$fileName")
)
}.toOption
}

private def getBloopGlobalJsonLastModifiedTime(
bloopGlobalJsonFilePath: AbsolutePath
): Long =
Try {
bloopGlobalJsonFilePath.toNIO.toFile.lastModified()
}.getOrElse(0)

/**
* First we check if the user requested to update the Bloop JVM
* properties through the extension.
* <p>If so, we also check if the Bloop's Global Json file exists
* and if it was pre-modified by the user.
* <p>Then, through consultation with the user through appropriate
* dialogues we decide if we should
* <ul>
* <li>overwrite the contents of the Bloop Global Json file with the
* requested JVM properties and Metal's JavaHome variables; and then
* restart the Bloop server</li>
* <li>or alternatively, leave things untouched</li>
* </ul>
*
* @param maybeRequestedBloopJvmProperties Bloop JVM Properties requested
* through the Metals Extension settings
* @param maybeRunningBloopJvmProperties
* @param maybeJavaHome Metals' `javaHome`, which Bloop
* should also preferebly use
* @param reconnect function to connect back to the
* build server.
* @return `Future.successful` if the purpose is achieved or `Future.failure`
* if a problem occured such as lacking enough permissions to open or
* write to files
*/
def ensureDesiredJvmSettings(
maybeRequestedBloopJvmProperties: Option[List[String]],
maybeRunningBloopJvmProperties: Option[List[String]],
maybeJavaHome: Option[String],
zmerr marked this conversation as resolved.
Show resolved Hide resolved
reconnect: () => Future[BuildChange]
): Future[Unit] = {
val result =
for { // if the requestedBloopJvmProperties and bloopGlobalJsonFilePath are defined
requestedBloopJvmProperties <- maybeRequestedBloopJvmProperties
bloopGlobalJsonFilePath <- getBloopFilePath(fileName = "bloop.json")
bloopCreatedByMetalsFilePath <- getBloopFilePath(fileName =
"created_by_metals.lock"
)
} yield
if (
maybeRequestedBloopJvmProperties != maybeRunningBloopJvmProperties && requestedBloopJvmProperties.nonEmpty
) { // the properties are updated
if (
bloopGlobalJsonFilePath.exists &&
getBloopGlobalJsonLastModifiedTime(
bloopGlobalJsonFilePath
) > getBloopGlobalJsonLastModifiedByMetalsTime(
bloopCreatedByMetalsFilePath
)
) {
// the global json file was previously modified by the user through other means;
// therefore overwriting it requires user input
languageClient
.showMessageRequest(
Messages.BloopGlobalJsonFilePremodified.params()
)
.asScala
.flatMap {
processUserPreferenceForBloopJvmProperties(
_,
bloopGlobalJsonFilePath,
bloopCreatedByMetalsFilePath,
requestedBloopJvmProperties,
maybeJavaHome,
reconnect
) andThen {
case Failure(exception) =>
languageClient.showMessage(
MessageType.Error,
exception.getMessage
)
case Success(_) => Future.successful()
}
}
} else {
// bloop global json file did not exist; or it was last modified by metals;
// hence it can get created or overwritten by Metals with no worries
// about overriding the user preferred settings
updateBloopGlobalJsonFileThenRestart(
bloopGlobalJsonFilePath,
bloopCreatedByMetalsFilePath,
requestedBloopJvmProperties,
maybeJavaHome,
reconnect
) andThen {
case Failure(exception) =>
languageClient.showMessage(
MessageType.Error,
exception.getMessage
)
case Success(_) => Future.successful()
}
}
} else Future.successful(())

result.getOrElse(Future.successful(()))

}

private def connectToLauncher(
bloopVersion: String,
bloopPort: Option[Int]
Expand All @@ -141,9 +368,11 @@ final class BloopServers(
val serverStarted = Promise[Unit]()
val bloopLogs = new OutputStream {
private lazy val b = new StringBuilder

override def write(byte: Int): Unit = byte.toChar match {
case c => b.append(c)
}

def logs = b.result.linesIterator
}

Expand Down
46 changes: 46 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,52 @@ object Messages {
}
}

object BloopGlobalJsonFilePremodified {
def applyAndRestart: MessageActionItem =
new MessageActionItem("Apply and Restart Bloop")
def useGlobalFile: MessageActionItem =
new MessageActionItem("Use the Global File's JVM Properties")
def openGlobalJsonFile: MessageActionItem =
new MessageActionItem("Open the Global File")
def params(): ShowMessageRequestParams = {
val params = new ShowMessageRequestParams()
params.setMessage(
s"""|You have previously modified the JVM settings of Bloop in its global json file,
|Do you want to replace them with the new properties and restart the running Bloop server?""".stripMargin
)
params.setType(MessageType.Warning)
params.setActions(
List(
applyAndRestart,
useGlobalFile,
openGlobalJsonFile
).asJava
)
params
}
}

object BloopJvmPropertiesChange {
def reconnect: MessageActionItem =
new MessageActionItem("Restart Bloop")
def notNow: MessageActionItem =
new MessageActionItem("Not now")
def params(): ShowMessageRequestParams = {
val params = new ShowMessageRequestParams()
params.setMessage(
s"New Bloop JVM properties detected. Bloop will need to be restarted in order for them to take effect."
)
params.setType(MessageType.Warning)
params.setActions(
List(
reconnect,
notNow
).asJava
)
params
}
}

object AmmoniteJvmParametersChange {
def restart: MessageActionItem =
new MessageActionItem("Restart Ammonite")
Expand Down
Loading