All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bleep.bsp.BspImpl.scala Maven / Gradle / Ivy

package bleep
package bsp

import bleep.internal.logException
import bloop.rifle.*
import bloop.rifle.internal.Operations
import ch.epfl.scala.bsp4j
import org.eclipse.lsp4j.jsonrpc

import java.net.Socket
import java.nio.file.{Files, Path}
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}

object BspImpl {
  def run(pre: Prebootstrapped): ExitCode = {
    val bspBloopServer: BleepBspServer =
      new BleepBspServer(pre.logger, null, null, null)

    bsp.BspThreads.withThreads { threads =>
      val launcher = new jsonrpc.Launcher.Builder[bsp4j.BuildClient]()
        .setExecutorService(threads.buildThreads.jsonrpc)
        .setInput(System.in)
        .setOutput(System.out)
        .setRemoteInterface(classOf[bsp4j.BuildClient])
        .setLocalService(bspBloopServer)
        .create()

      val localClient = new BspForwardClient(Some(launcher.getRemoteProxy), pre.logger)

      val config = BleepConfigOps.loadOrDefault(pre.userPaths).orThrow

      val build = pre.existingBuild.buildFile.forceGet.getOrElse {
        model.BuildFile(model.$schema, model.BleepVersion.current, model.JsonMap.empty, model.JsonMap.empty, model.JsonList.empty, model.JsonMap.empty, None)
      }
      val bleepRifleLogger = new BleepRifleLogger(pre.logger)
      val resolver = CoursierResolver.Factory.default(pre, config, build)
      val bloopRifleConfig =
        SetupBloopRifle(
          compileServerMode = config.compileServerModeOrDefault,
          resolvedJvm = pre.resolvedJvm.forceGet,
          userPaths = pre.userPaths,
          resolver = resolver,
          bleepRifleLogger = bleepRifleLogger
        )

      val workspaceDir = pre.buildPaths.buildVariantDir
      val buildChangeTracker = BuildChangeTracker.make(config, pre, localClient)
      // Adding a file watcher that will update bloop config on the fly and
      // notify the BSP client of changes
      threads.prepareBuildExecutor.submit {
        new Runnable {
          def run(): Unit =
            BleepFileWatching
              .build(pre) { _ =>
                buildChangeTracker.ensureBloopUpToDate() match {
                  case Left(th) => logException("Could not reload build", pre.logger, th)
                  case Right(_) => pre.logger.info("Loaded changed build")
                }
              }
              .run(FileWatching.StopWhen.Never, 100)
        }
      }

      var bloopServer: BloopServer = null
      try {
        bloopServer = buildServer(bloopRifleConfig, workspaceDir, localClient, threads.buildThreads, bleepRifleLogger)
        bspBloopServer.sendToIdeClient = launcher.getRemoteProxy
        bspBloopServer.bloopServer = bloopServer.server
        // run on `workspaceBuildTargets` later for the side-effect of updating build
        bspBloopServer.buildChangeTracker = buildChangeTracker

        pre.logger.info {
          val hasConsole = System.console() != null
          if (hasConsole)
            "Listening to incoming JSONRPC BSP requests, press Ctrl+D to exit."
          else
            "Listening to incoming JSONRPC BSP requests."
        }

        val es = ExecutionContext.fromExecutorService(threads.buildThreads.jsonrpc)
        val doneFuture = Future.firstCompletedOf(
          Seq(
            BspImpl.naiveJavaFutureToScalaFuture(launcher.startListening()).map(_ => ())(es),
            bspBloopServer.initiateShutdown
          )
        )(es)

        Await.result(doneFuture, Duration.Inf)
        ExitCode.Success
      } finally {
        if (bloopServer != null)
          bloopServer.shutdown()

        try
          Operations.exit(
            bloopRifleConfig.address,
            workspaceDir,
            System.out,
            System.err,
            bleepRifleLogger
          )
        catch {
          case th: Throwable => pre.logger.warn("Couldn't exit bloop server at Bleep shutdown", th)
        }
        ()
      }
    }
  }

  // from https://githubsp4j.com/com-lihaoyi/Ammonite/blob/7eb58c58ec8c252dc5bd1591b041fcae01cccf90/amm/interp/src/main/scala/ammonite/interp/script/AmmoniteBuildServer.scala#L550-L565
  private def naiveJavaFutureToScalaFuture[T](f: java.util.concurrent.Future[T]): Future[T] = {
    val p = Promise[T]()
    val t = new Thread {
      setDaemon(true)
      setName("bsp-wait-for-exit")
      override def run(): Unit =
        p.complete {
          try Success(f.get())
          catch { case t: Throwable => Failure(t) }
        }
    }
    t.start()
    p.future
  }

  def buildServer(config: BloopRifleConfig, workspace: Path, buildClient: bsp4j.BuildClient, threads: BloopThreads, logger: BloopRifleLogger): BloopServer = {

    val (conn, socket, bloopInfo) =
      try BloopServer.bsp(config, workspace, threads, logger, config.period, config.timeout)
      catch {
        case th: FailedToStartServerException =>
          val readLog: Option[String] =
            config.address match {
              case _: BloopRifleConfig.Address.Tcp           => None
              case ds: BloopRifleConfig.Address.DomainSocket => Try(Files.readString(ds.outputPath)).toOption
            }
          throw BleepCommandRemote.FailedToStartBloop(th, readLog)

      }

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

    // FIXME As of now, we don't detect when connection gets closed.
    // For TCP connections, this should be do-able with heartbeat messages
    // (to be added to BSP?).
    // For named sockets, the recv system call is supposed to allow to detect
    // that case, unlike the read system call. But the ipcsocket library that we use
    // for named sockets relies on read.

    val launcher = new jsonrpc.Launcher.Builder[BuildServer]()
      .setExecutorService(threads.jsonrpc)
      .setInput(socket.getInputStream)
      .setOutput(socket.getOutputStream)
      .setRemoteInterface(classOf[BuildServer])
      .setLocalService(buildClient)
      .create()
    val server = launcher.getRemoteProxy

    val f = launcher.startListening()

    case class BloopServerImpl(
        server: BuildServer,
        listeningFuture: java.util.concurrent.Future[Void],
        socket: Socket,
        bloopInfo: BloopRifle.BloopServerRuntimeInfo
    ) extends BloopServer {
      def shutdown(): Unit = {
        // Close the jsonrpc thread listening to input messages
        // First line makes jsonrpc discard the closed connection exception.
        listeningFuture.cancel(true)
        socket.close()
      }
    }
    BloopServerImpl(server, f, socket, bloopInfo)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy