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

Bsp errors in status #5726

Merged
merged 8 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 149 additions & 16 deletions metals/src/main/scala/scala/meta/internal/bsp/ConnectionBspStatus.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package scala.meta.internal.bsp

import java.util.concurrent.atomic.AtomicBoolean
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicReference

import scala.meta.internal.metals.BspStatus
import scala.meta.internal.metals.Icons
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.ReportContext
import scala.meta.internal.metals.ServerCommands
import scala.meta.internal.metals.clients.language.MetalsStatusParams
import scala.meta.internal.metals.clients.language.StatusType
Expand All @@ -13,25 +16,83 @@ class ConnectionBspStatus(
bspStatus: BspStatus,
folderPath: AbsolutePath,
icons: Icons,
) {
private val isServerResponsive = new AtomicBoolean(false)
val status: MetalsStatusParams => Unit = bspStatus.status(folderPath, _)

def connected(serverName: String): Unit =
if (isServerResponsive.compareAndSet(false, true))
status(ConnectionBspStatus.connectedParams(serverName, icons))
def noResponse(serverName: String): Unit =
if (isServerResponsive.compareAndSet(true, false)) {
scribe.debug("server liveness monitor detected no response")
status(ConnectionBspStatus.noResponseParams(serverName, icons))
}
)(implicit rc: ReportContext) {
private val status = new AtomicReference[BspStatusState](
BspStatusState(Disconnected, None, None)
)
private val currentSessionErrors = new AtomicReference[Set[String]](Set())

def connected(serverName: String): Unit = changeState(Connected(serverName))

def noResponse(): Unit = {
scribe.debug("server liveness monitor detected no response")
changeState(NoResponse)
}

def disconnected(): Unit = {
isServerResponsive.set(false)
status(ConnectionBspStatus.disconnectedParams)
currentSessionErrors.set(Set.empty)
changeState(Disconnected)
}

def showError(message: String, pathToReport: Path): Unit = {
val updatedSet =
currentSessionErrors.updateAndGet(_ + pathToReport.toUri().toString())
changeState(ErrorMessage(message), updatedSet)
}

def onReportsUpdate(): Unit = {
jkciesluk marked this conversation as resolved.
Show resolved Hide resolved
val currentState = status.get()
currentState.currentState match {
case error @ ErrorMessage(_) => showState(error, currentState)
case _ =>
}
}

def isBuildServerResponsive: Option[Boolean] =
status.get().currentState match {
case NoResponse => Some(false)
case Disconnected => None
case _ => Some(true)
}

private def changeState(
newState: BspServerState,
errorReports: Set[String] = currentSessionErrors.get(),
) = {
val old = status.getAndUpdate(_.changeState(newState)._2)
val (newServerState, newBspState) = old.changeState(newState)
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
newServerState.foreach(showState(_, newBspState, errorReports))
}

def isBuildServerResponsive: Boolean = isServerResponsive.get()
private def showState(
state: BspServerState,
currState: BspStatusState,
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
errorReports: Set[String] = currentSessionErrors.get(),
) = {
def serverName = currState.serverName.getOrElse("bsp")
val showParams =
state match {
case Disconnected => ConnectionBspStatus.disconnectedParams
case NoResponse =>
ConnectionBspStatus.noResponseParams(serverName, icons)
case Connected(serverName) =>
ConnectionBspStatus.connectedParams(serverName, icons)
case ErrorMessage(message) =>
val currentSessionReports = errorReports.intersect(
tgodzik marked this conversation as resolved.
Show resolved Hide resolved
rc.bloop.getReports().map(_.toPath.toUri().toString()).toSet
)
if (currentSessionReports.isEmpty)
ConnectionBspStatus.connectedParams(serverName, icons)
else
ConnectionBspStatus.bspErrorParams(
serverName,
icons,
message,
currentSessionReports.size,
)
}
bspStatus.status(folderPath, showParams)
}
}

object ConnectionBspStatus {
Expand All @@ -55,4 +116,76 @@ object ConnectionBspStatus {
command = ServerCommands.ConnectBuildServer.id,
commandTooltip = "Reconnect.",
).withStatusType(StatusType.bsp)

def bspErrorParams(
serverName: String,
icons: Icons,
message: String,
errorsNumber: Int,
): MetalsStatusParams =
MetalsStatusParams(
s"$serverName $errorsNumber ${icons.alert}",
"warn",
show = true,
tooltip = message.trimTo(TOOLTIP_MAX_LENGTH),
command = ServerCommands.RunDoctor.id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow up, would it be possible (probably on vs-code side) to make doctor open on the part with error reports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that sounds like a good idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also open them in that case, currently they are folded.

commandTooltip = "Open doctor.",
).withStatusType(StatusType.bsp)

private val TOOLTIP_MAX_LENGTH = 150
}

trait BspServerState
case object Disconnected extends BspServerState
case class Connected(serverName: String) extends BspServerState
case class ErrorMessage(message: String) extends BspServerState
case object NoResponse extends BspServerState

case class BspStatusState(
currentState: BspServerState,
previousState: Option[BspServerState],
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
serverName: Option[String],
kasiaMarek marked this conversation as resolved.
Show resolved Hide resolved
) {
def changeState(
newState: BspServerState
): (Option[BspServerState], BspStatusState) = {
newState match {
case Disconnected if currentState != Disconnected => moveTo(Disconnected)
case NoResponse if currentState != NoResponse => moveTo(NoResponse)
case ErrorMessage(msg) =>
currentState match {
case NoResponse =>
(
None,
BspStatusState(NoResponse, Some(ErrorMessage(msg)), serverName),
)
case _ => moveTo(ErrorMessage(msg))
}
case Connected(serverName) =>
currentState match {
case Disconnected => moveTo(Connected(serverName))
case NoResponse =>
previousState match {
case Some(ErrorMessage(msg)) => moveTo(ErrorMessage(msg))
case _ => moveTo(Connected(serverName))
}
case _ => (None, this)
}
case _ => (None, this)
}
}

def moveTo(
newState: BspServerState
): (Some[BspServerState], BspStatusState) = {
val newServerName =
newState match {
case Connected(serverName) => Some(serverName)
case _ => serverName
}
(
Some(newState),
BspStatusState(newState, Some(currentState), newServerName),
)
}
}
154 changes: 29 additions & 125 deletions metals/src/main/scala/scala/meta/internal/builds/BSPErrorHandler.scala
Original file line number Diff line number Diff line change
@@ -1,49 +1,26 @@
package scala.meta.internal.builds

import java.nio.file.Files
import java.util.concurrent.atomic.AtomicReference

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.control.NonFatal
import java.security.MessageDigest

import scala.meta.internal.bsp.BspSession
import scala.meta.internal.metals.ClientCommands
import scala.meta.internal.metals.ConcurrentHashSet
import scala.meta.internal.metals.Directories
import scala.meta.internal.bsp.ConnectionBspStatus
import scala.meta.internal.metals.MetalsEnrichments._
import scala.meta.internal.metals.Report
import scala.meta.internal.metals.ReportContext
import scala.meta.internal.metals.Tables
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
import scala.meta.io.AbsolutePath

import org.eclipse.{lsp4j => l}

class BspErrorHandler(
languageClient: MetalsLanguageClient,
workspaceFolder: AbsolutePath,
currentSession: () => Option[BspSession],
tables: Tables,
)(implicit context: ExecutionContext) {

protected def logsPath: AbsolutePath =
workspaceFolder.resolve(Directories.log)
private val lastError = new AtomicReference[String]("")
private val dismissedErrors = ConcurrentHashSet.empty[String]

def onError(message: String): Future[Unit] = {
if (
!tables.dismissedNotifications.BspErrors.isDismissed &&
shouldShowBspError &&
!dismissedErrors.contains(message)
) {
val previousError = lastError.getAndSet(message)
if (message != previousError) {
showError(message)
} else Future.successful(())
} else {
logError(message)
Future.successful(())
}
bspStatus: ConnectionBspStatus,
)(implicit reportContext: ReportContext) {
def onError(message: String): Unit = {
if (shouldShowBspError) {
for {
report <- createReport(message)
if !tables.dismissedNotifications.BspErrors.isDismissed
} bspStatus.showError(message, report)
} else logError(message)
}

def shouldShowBspError: Boolean = currentSession().exists(session =>
Expand All @@ -52,96 +29,23 @@ class BspErrorHandler(

protected def logError(message: String): Unit = scribe.error(message)

private def showError(message: String): Future[Unit] = {
val bspError = s"${BspErrorHandler.errorInBsp} $message"
logError(bspError)
val params = BspErrorHandler.makeShowMessage(message)
languageClient.showMessageRequest(params).asScala.flatMap {
case BspErrorHandler.goToLogs =>
val errorMsgStartLine =
bspError.linesIterator.headOption
.flatMap(findLine(_))
.getOrElse(0)
Future.successful(gotoLogs(errorMsgStartLine))
case BspErrorHandler.dismiss =>
Future.successful(dismissedErrors.add(message)).ignoreValue
case BspErrorHandler.doNotShowErrors =>
Future.successful {
tables.dismissedNotifications.BspErrors.dismissForever
}.ignoreValue
case _ => Future.successful(())
}
}

private def findLine(line: String): Option[Int] =
try {
val lineNumber =
Files
.readAllLines(logsPath.toNIO)
.asScala
.lastIndexWhere(_.contains(line))
if (lineNumber >= 0) Some(lineNumber) else None
} catch {
case NonFatal(_) => None
}

private def gotoLogs(line: Int) = {
val pos = new l.Position(line, 0)
val location = new l.Location(
logsPath.toURI.toString(),
new l.Range(pos, pos),
)
languageClient.metalsExecuteClientCommand(
ClientCommands.GotoLocation.toExecuteCommandParams(
ClientCommands.WindowLocation(
location.getUri(),
location.getRange(),
)
private def createReport(message: String) = {
val id = MessageDigest
.getInstance("MD5")
.digest(message.getBytes)
.map(_.toChar)
.mkString
val sanitized = reportContext.bloop.sanitize(message)
reportContext.bloop.create(
Report(
sanitized.trimTo(20),
s"""|### Bloop error:
|
|$message""".stripMargin,
shortSummary = sanitized.trimTo(100),
path = None,
id = Some(id),
)
)
}
}

object BspErrorHandler {
def makeShowMessage(
message: String
): l.ShowMessageRequestParams = {
val (msg, actions) =
if (message.length() <= MESSAGE_MAX_LENGTH) {
(
makeShortMessage(message),
List(dismiss, doNotShowErrors),
)
} else {
(
makeLongMessage(message),
List(goToLogs, dismiss, doNotShowErrors),
)
}
val params = new l.ShowMessageRequestParams()
params.setType(l.MessageType.Error)
params.setMessage(msg)
params.setActions(actions.asJava)
params
}

def makeShortMessage(message: String): String =
s"""|$errorHeader
|$message""".stripMargin

def makeLongMessage(message: String): String =
s"""|${makeShortMessage(s"${message.take(MESSAGE_MAX_LENGTH)}...")}
|$gotoLogsToSeeFull""".stripMargin

val goToLogs = new l.MessageActionItem("Go to logs.")
val dismiss = new l.MessageActionItem("Dismiss.")
val doNotShowErrors = new l.MessageActionItem("Stop showing bsp errors.")

val errorHeader = "Encountered an error in the build server:"
private val gotoLogsToSeeFull = "Go to logs to see the full error"

val errorInBsp = "Build server error:"

private val MESSAGE_MAX_LENGTH = 150

}
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,6 @@ class BuildServerConnection private (
}
}

def isBuildServerResponsive: Future[Option[Boolean]] = {
val original = connection
original.map(
_.optLivenessMonitor
.map(_.isBuildServerResponsive)
)
}

}

object BuildServerConnection {
Expand Down Expand Up @@ -494,7 +486,6 @@ object BuildServerConnection {
config.metalsToIdleTime,
config.pingInterval,
bspStatus,
serverName,
)

LauncherConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ object Configs {
new FileSystemWatcher(
Either.forLeft(s"$root/project/build.properties")
),
new FileSystemWatcher(
Either.forLeft(s"$root/.metals/.reports/bloop/*/*")
),
).asJava
)
}
Expand Down
Loading