From de19c5bb850ebb45c2c84be85bab22d6017003cc Mon Sep 17 00:00:00 2001 From: Aish Date: Fri, 6 Jan 2023 18:29:43 -0800 Subject: [PATCH] Exit BspServerApp cleanly (#13) * Resolved issue where bsp-server wasn't shutting down cleanly. --- build.sbt | 4 +-- examples/simple-no-errors/.bsp/bazel-bsp.json | 3 +- .../simple-with-errors/.bsp/bazel-bsp.json | 3 +- .../scala/afenton/bazel/bsp/BazelBspApp.scala | 35 ++++++++++--------- .../afenton/bazel/bsp/BazelBspServer.scala | 15 ++++---- .../afenton/bazel/bsp/BuildMetaData.scala | 2 +- .../scala/afenton/bazel/bsp/End2EndTest.scala | 3 +- .../afenton/bazel/bsp/LspTestProcess.scala | 15 ++++---- 8 files changed, 46 insertions(+), 34 deletions(-) diff --git a/build.sbt b/build.sbt index 4c258f6..81b1dbe 100644 --- a/build.sbt +++ b/build.sbt @@ -7,8 +7,8 @@ lazy val root = project .in(file(".")) .settings( name := "bazel-bsp", - organization := "org.afenton", - version := "0.1.0-SNAPSHOT", + organization := "afenton", + version := "0.0.15", scalaVersion := scala3Version, libraryDependencies += "org.typelevel" %% "cats-effect" % "3.4.4", libraryDependencies += "co.fs2" %% "fs2-core" % "3.3.0", diff --git a/examples/simple-no-errors/.bsp/bazel-bsp.json b/examples/simple-no-errors/.bsp/bazel-bsp.json index d9d8b87..c970f82 100644 --- a/examples/simple-no-errors/.bsp/bazel-bsp.json +++ b/examples/simple-no-errors/.bsp/bazel-bsp.json @@ -7,6 +7,7 @@ "scala" ], "argv": [ - "bazel-bsp" + "../../target/universal/stage/bin/bazel-bsp", + "--verbose" ] } diff --git a/examples/simple-with-errors/.bsp/bazel-bsp.json b/examples/simple-with-errors/.bsp/bazel-bsp.json index d9d8b87..c970f82 100644 --- a/examples/simple-with-errors/.bsp/bazel-bsp.json +++ b/examples/simple-with-errors/.bsp/bazel-bsp.json @@ -7,6 +7,7 @@ "scala" ], "argv": [ - "bazel-bsp" + "../../target/universal/stage/bin/bazel-bsp", + "--verbose" ] } diff --git a/src/main/scala/afenton/bazel/bsp/BazelBspApp.scala b/src/main/scala/afenton/bazel/bsp/BazelBspApp.scala index 711fe5f..a1070c3 100644 --- a/src/main/scala/afenton/bazel/bsp/BazelBspApp.scala +++ b/src/main/scala/afenton/bazel/bsp/BazelBspApp.scala @@ -24,6 +24,7 @@ import fs2.text import io.bazel.rules_scala.diagnostics.diagnostics.FileDiagnostics import io.circe.Json import io.circe.syntax.* +import cats.effect.std.Console import java.net.ServerSocket import java.nio.file.Path @@ -32,6 +33,7 @@ import cats.data.Nested import java.nio.file.StandardOpenOption import scala.concurrent.duration.FiniteDuration import java.util.concurrent.TimeUnit +import cats.effect.kernel.Deferred object BazelBspApp extends CommandIOApp( @@ -50,14 +52,14 @@ object BazelBspApp fs2.text.utf8.encode.andThen(fs2.io.stdout), fs2.text.utf8.encode.andThen(fs2.io.stderr) ) - .handleErrorWith { e => - IO.blocking { - System.err.println( - s"ERROR: 💣 💣 💣 \n${e.toString} \n${e.getStackTrace.mkString("\n")}" - ) - ExitCode.Error + .handleErrorWith { e => + IO.blocking { + System.err.println( + s"ERROR: 💣 💣 💣 \n${e.toString} \n${e.getStackTrace.mkString("\n")}" + ) + ExitCode.Error + } } - } } val verifySetupOpt = @@ -87,11 +89,11 @@ object BazelBspApp def printVerifyResult( result: List[(String, Either[String, Unit])] ): IO[Unit] = - result - .traverse_ { - case (n, Right(_)) => IO.println(s"✅ $n") - case (n, Left(err)) => IO.println(s"❌ $n\n $err\n") - } + result + .traverse_ { + case (n, Right(_)) => IO.println(s"✅ $n") + case (n, Left(err)) => IO.println(s"❌ $n\n $err\n") + } def main: Opts[IO[ExitCode]] = verboseOpt @@ -140,14 +142,13 @@ object BazelBspApp logger: Logger ): Stream[IO, Unit] = inStream - // .evalTap(s => logger.trace(s)) .through(jRpcParser(logger)) .evalTap(request => logger.trace(s"request: ${request}")) .through(messageDispatcher(BspServer.jsonRpcRouter(bspServer))) .evalTap(response => logger.trace(s"response: ${response}")) .evalMap { case resp: Response => outQ.offer(resp) - case u: Unit => IO.unit + case u: Unit => IO.unit } private def processOutStream( @@ -158,7 +159,6 @@ object BazelBspApp Stream .fromQueueUnterminated(outQ, 100) .map(msg => JRpcConsoleCodec.encode(msg, false)) - // .evalTap(msg => logger.trace(msg)) .through(outPipe) def server(verbose: Boolean)( @@ -177,5 +177,8 @@ object BazelBspApp processOutStream(outPipe, outQ, logger), logStream ).parJoin(3) + // NB: Allow time for response to get through + .interruptWhen(server.exitSignal) + .onFinalize(Console[IO].errorln("👋 BSP Server Shutting Down")) _ <- all.compile.drain - yield ExitCode.Success \ No newline at end of file + yield ExitCode.Success diff --git a/src/main/scala/afenton/bazel/bsp/BazelBspServer.scala b/src/main/scala/afenton/bazel/bsp/BazelBspServer.scala index 11fa827..62bea7f 100644 --- a/src/main/scala/afenton/bazel/bsp/BazelBspServer.scala +++ b/src/main/scala/afenton/bazel/bsp/BazelBspServer.scala @@ -19,13 +19,15 @@ import io.circe.syntax._ import java.net.URI import java.nio.file.Path import java.nio.file.Paths +import cats.effect.kernel.Deferred import IOLifts.{asIO, mapToIO} class BazelBspServer( client: BspClient, logger: Logger, - stateRef: Ref[IO, BazelBspServer.ServerState] + stateRef: Ref[IO, BazelBspServer.ServerState], + val exitSignal: Deferred[IO, Either[Throwable, Unit]] ) extends BspServer(client): def buildInitialize( @@ -252,7 +254,7 @@ class BazelBspServer( def buildExit(params: Unit): IO[Unit] = for _ <- logger.info("build/exit") - _ <- IO.canceled + _ <- exitSignal.complete(Right(())) yield () private def doBuildTargetSources( @@ -330,10 +332,11 @@ object BazelBspServer: ServerState(BazelBspServer.TargetSourceMap.empty, Nil, None, None, Nil) def create(client: BspClient, logger: Logger): IO[BazelBspServer] = - Ref.of[IO, BazelBspServer.ServerState](defaultState) - .map { stateRef => - BazelBspServer(client, logger, stateRef) - } + for + exitSwitch <- Deferred[IO, Either[Throwable, Unit]] + stateRef <- Ref.of[IO, BazelBspServer.ServerState](defaultState) + yield + BazelBspServer(client, logger, stateRef, exitSwitch) protected case class TargetSourceMap( val _targetSources: Map[BuildTargetIdentifier, List[ diff --git a/src/main/scala/afenton/bazel/bsp/BuildMetaData.scala b/src/main/scala/afenton/bazel/bsp/BuildMetaData.scala index 2f6d5dc..f0f1831 100644 --- a/src/main/scala/afenton/bazel/bsp/BuildMetaData.scala +++ b/src/main/scala/afenton/bazel/bsp/BuildMetaData.scala @@ -1,6 +1,6 @@ package afenton.bazel.bsp object BuildMetaData { - val Version = "0.0.13" + val Version = "0.0.15" val BspVersion = "2.0.0-M2" } \ No newline at end of file diff --git a/src/test/scala/afenton/bazel/bsp/End2EndTest.scala b/src/test/scala/afenton/bazel/bsp/End2EndTest.scala index 618cfd2..02fde85 100644 --- a/src/test/scala/afenton/bazel/bsp/End2EndTest.scala +++ b/src/test/scala/afenton/bazel/bsp/End2EndTest.scala @@ -23,7 +23,8 @@ import scala.concurrent.duration._ class End2EndTest extends munit.CatsEffectSuite with BspHelpers: - override val munitTimeout = 6.minute + // Long, because Github actions can run slooooow at times + override val munitTimeout = 10.minute val projectRoot = Paths.get("").toAbsolutePath diff --git a/src/test/scala/afenton/bazel/bsp/LspTestProcess.scala b/src/test/scala/afenton/bazel/bsp/LspTestProcess.scala index 085195c..4814910 100644 --- a/src/test/scala/afenton/bazel/bsp/LspTestProcess.scala +++ b/src/test/scala/afenton/bazel/bsp/LspTestProcess.scala @@ -46,6 +46,12 @@ case class BspClient( bspInQ: Queue[IO, String], counter: Ref[IO, Int] ): + private def sendNotification[A: Encoder]( + method: String, + params: A + ): IO[Unit] = + val notification = Notification("2.0", method, Some(Encoder[A].apply(params))) + bspInQ.offer(JRpcConsoleCodec.encode(notification, false)) private def sendRequest[A: Encoder, B: Decoder]( method: String, @@ -92,9 +98,8 @@ case class BspClient( def buildShutdown(params: Unit): IO[DeferredSource[IO, Unit]] = sendRequest("build/shutdown", params) - // TODO: Need to turn this into a notification, not a request - // def buildExit(params: Unit): IO[Unit] = - // sendRequest("build/exit", params) + def buildExit(params: Unit): IO[Unit] = + sendNotification("build/exit", params) // def workspaceBuildTargets(params: Unit): IO[WorkspaceBuildTargetsResult] @@ -261,9 +266,7 @@ case class LspTestProcess(workspaceRoot: Path): for d1 <- client.buildShutdown(()) _ <- d1.get - // TODO need to send exit, once that's supported - // d2 <- client.buildExit(()) - // _ <- d2.get + _ <- client.buildExit(()) _ <- exitSwitch.complete(Right(())) yield ()