summaryrefslogtreecommitdiff
path: root/contrib/bsp/src/mill/contrib/MainMillBuildServer.scala
blob: aa4cf2119c7d4c67364b4d69aabbe23d64794e16 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package mill.contrib

import play.api.libs.json._
import java.nio.file.FileAlreadyExistsException
import java.util.concurrent.Executors
import upickle.default._
import ch.epfl.scala.bsp4j._
import mill._
import mill.define.{Command, Discover, ExternalModule}
import mill.eval.Evaluator
import org.eclipse.lsp4j.jsonrpc.Launcher
import scala.collection.JavaConverters._


object BSP extends ExternalModule {

  implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]()

  lazy val millDiscover: Discover[BSP.this.type] = Discover[this.type]
  val version = "1.0.0"
  val bspVersion = "2.0.0"
  val languages = List("scala", "java")

  // computes the path to the java executable
  def whichJava: String = {
    if (scala.sys.props.contains("JAVA_HOME")) scala.sys.props("JAVA_HOME") else "java"
  }

  // creates a Json with the BSP connection details
  def createBspConnectionJson(): JsValue = {

    implicit val connectionWrites = new Writes[BspConnectionDetails] {
      def writes(connection: BspConnectionDetails) = Json.obj(
        "name" -> connection.getName,
        "argv" -> new JsArray(connection.getArgv.asScala.map(string => JsString(string)).toIndexedSeq),
        "version" -> connection.getVersion,
        "bspVersion" -> connection.getBspVersion,
        "languages" -> new JsArray(connection.getLanguages.asScala.map(string => JsString(string)).toIndexedSeq)
      )
    }
    val millPath = scala.sys.props("MILL_CLASSPATH")
    Json.toJson(new BspConnectionDetails("mill-bsp",
                                          List(whichJava,"-DMILL_CLASSPATH=" + millPath,
                                            s"-DMILL_VERSION=${scala.sys.props("MILL_VERSION")}",
                                            "-Djna.nosys=true", "-cp",
                                            millPath,
                                            "mill.MillMain", "mill.contrib.BSP/start").asJava,
                                          version,
                                          bspVersion,
                                          languages.asJava))
  }

  /**
    * Installs the mill-bsp server. It creates a json file
    * with connection details in the ./.bsp directory for
    * a potential client to find.
    *
    * If a .bsp folder with a connection file already
    * exists in the working directory, it will be
    * overwritten and a corresponding message will be displayed
    * in stdout.
    *
    * If the creation of the .bsp folder fails due to any other
    * reason, the message and stacktrace of the exception will be
    * printed to stdout.
    *
    */
  def install(ev: Evaluator): Command[Unit] = T.command{
    val bspDirectory = os.pwd / ".bsp"
    if (! os.exists(bspDirectory)) os.makeDir.all(bspDirectory)
    try {
      os.write(bspDirectory / "mill.json", Json.stringify(createBspConnectionJson()))
    } catch {
      case e: FileAlreadyExistsException =>
        println("The bsp connection json file probably exists already - will be overwritten")
        os.remove(bspDirectory / "mill.json")
        os.write(bspDirectory / "mill.json", Json.stringify(createBspConnectionJson()))
      case e: Exception => println("An exception occurred while installing mill-bsp: " + e.getMessage +
                                  " " + e.getStackTrace.toString)
    }

  }

  /**
    * Computes a mill command which starts the mill-bsp
    * server and establishes connection to client. Waits
    * until a client connects and ends the connection
    * after the client sent an "exit" notification
    * @param ev Environment, used by mill to evaluate commands
    * @return: mill.Command which executes the starting of the
    *         server
    */
  def start(ev: Evaluator): Command[Unit] = T.command {
    val eval = new Evaluator(ev.home, ev.outPath, ev.externalOutPath, ev.rootModule, ev.log, ev.classLoaderSig,
                    ev.workerCache, ev.env, false)
    val millServer = new mill.contrib.bsp.MillBuildServer(eval, bspVersion, version, languages)
    val executor = Executors.newCachedThreadPool()

    val stdin = System.in
    val stdout = System.out
    try {
      val launcher = new Launcher.Builder[BuildClient]()
        .setOutput(stdout)
        .setInput(stdin)
        .setLocalService(millServer)
        .setRemoteInterface(classOf[BuildClient])
        .setExecutorService(executor)
        .create()
      millServer.onConnectWithClient(launcher.getRemoteProxy)
      val listening = launcher.startListening()
      millServer.cancelator = () => listening.cancel(true)
      val voidFuture = listening.get()
    } catch {
      case e: Exception =>
        System.err.println("An exception occured while connecting to the client.")
        System.err.println("Cause: " + e.getCause)
        System.err.println("Message: " + e.getMessage)
        System.err.println("Exception class: " + e.getClass)
        System.err.println("Stack Trace: " + e.getStackTrace)
    } finally {
      System.err.println("Shutting down executor")
      executor.shutdown()
    }
  }
}