Skip to content

Commit

Permalink
Tracer MVP (#27)
Browse files Browse the repository at this point in the history
* Tracer MVP

* Module names and releasing

* Remove version override
  • Loading branch information
keynmol authored Aug 26, 2022
1 parent da8cc13 commit 6d4d207
Show file tree
Hide file tree
Showing 11 changed files with 896 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ concurrency:

env:
JAVA_OPTS: "-Xmx4G"
RELEASE: yesh

jobs:
build:
Expand Down
83 changes: 82 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ val V = new {
val cats = "2.8.0"
val verify = "1.0.0"
val jsonrpclib = "0.0.3"
val fs2 = "3.2.12"
val http4s = "0.23.15"
val laminar = "0.14.2"
}

lazy val noPublishing = Seq(
Expand All @@ -49,7 +52,10 @@ val default = Seq(VirtualAxis.scalaABIVersion(V.scala), VirtualAxis.jvm)

lazy val root = project
.in(file("."))
.aggregate((meta.projectRefs ++ lsp.projectRefs)*)
.aggregate(meta.projectRefs*)
.aggregate(lsp.projectRefs*)
.aggregate(tracer.projectRefs*)
.aggregate(tracerShared.projectRefs*)
.settings(noPublishing)

lazy val meta = projectMatrix
Expand Down Expand Up @@ -94,6 +100,74 @@ lazy val generate = projectMatrix
.jvmPlatform(scalaVersions)
.settings(noPublishing)

lazy val tracer = projectMatrix
.in(file("modules/tracer/backend"))
.dependsOn(lsp, tracerShared)
.defaultAxes(default*)
.settings(
name := "langoustine-tracer",
libraryDependencies += "tech.neander" %%% "jsonrpclib-fs2" % V.jsonrpclib,
libraryDependencies += "co.fs2" %%% "fs2-io" % V.fs2,
libraryDependencies += "org.http4s" %%% "http4s-ember-server" % V.http4s,
libraryDependencies += "org.http4s" %%% "http4s-dsl" % V.http4s,
// embedding frontend in backend's resources
Compile / resourceGenerators += {
Def.task[Seq[File]] {
val (_, location) = (ThisBuild / frontendOutput).value

val outDir = (Compile / resourceManaged).value / "assets"
IO.listFiles(location).toList.map { file =>
val (name, ext) = file.baseAndExt
val out = outDir / (name + "." + ext)

IO.copyFile(file, out)

out
}
}
}
)
.jvmPlatform(scalaVersions)

import org.scalajs.linker.interface.Report
lazy val frontendJS = tracerFrontend.js(V.scala)
lazy val isRelease = sys.env.get("RELEASE").contains("yesh")

lazy val frontendOutput = taskKey[(Report, File)]("")
ThisBuild / frontendOutput := {
if (isRelease)
(frontendJS / Compile / fullLinkJS).value.data ->
(frontendJS / Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
else
(frontendJS / Compile / fastLinkJS).value.data ->
(frontendJS / Compile / fastLinkJS / scalaJSLinkerOutputDirectory).value
}

lazy val tracerFrontend = projectMatrix
.in(file("modules/tracer/frontend"))
.dependsOn(tracerShared)
.defaultAxes(default*)
.settings(
name := "langoustine-tracer-frontend",
libraryDependencies += "com.raquo" %%% "laminar" % V.laminar,
scalaJSUseMainModuleInitializer := true
)
.jsPlatform(scalaVersions)

lazy val tracerShared = projectMatrix
.in(file("modules/tracer/shared"))
.defaultAxes(default*)
.settings(
name := "langoustine-tracer-shared",
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.15.0",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.15.0" % "compile-internal",
"tech.neander" %%% "jsonrpclib-core" % V.jsonrpclib
)
)
.jsPlatform(scalaVersions)
.jvmPlatform(scalaVersions)

lazy val docs = projectMatrix
.in(file("docs"))
.dependsOn(lsp)
Expand Down Expand Up @@ -192,3 +266,10 @@ lazy val docsSettings = Seq(
out
}
)

/* ThisBuild / version := { */
/* sys.env.get("VERSION_OVERRIDE") match { */
/* case None => version.value */
/* case Some(value) => value */
/* } */
/* } */
12 changes: 12 additions & 0 deletions modules/lsp/src/test/resources/textDocument/didOpen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"params": {
"textDocument": {
"uri": "file:///home/user/grammar.js",
"languageId": "javascript",
"version": 0,
"text": "hello"
}
},
"jsonrpc": "2.0",
"method": "textDocument/didOpen"
}
14 changes: 14 additions & 0 deletions modules/lsp/src/test/resources/textDocument/documentSymbol.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"id": 25,
"method": "textDocument/documentSymbol",
"params": {
"textDocument": {
"uri": "file:///home/user/README.md"
},
"position": {
"line": 14,
"character": 0
}
},
"jsonrpc": "2.0"
}
21 changes: 21 additions & 0 deletions modules/tracer/backend/src/main/resources/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>Langoustine tracer</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- <link rel="stylesheet" href="/path/to/styles/default.min.css"> -->
<!-- <script src="/path/to/highlight.min.js"></script> -->
<!-- <script>hljs.highlightAll();</script> -->
<style type = "text/css">
font-family: 'Wotfard',Futura,-apple-system,sans-serif;
</style>
</head>

<body>
<div id="appContainer"></div>
<script src="/assets/main.js"></script>
</body>

</html>
75 changes: 75 additions & 0 deletions modules/tracer/backend/src/main/scala/ChildProcess.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package langoustine.tracer

import fs2.Stream
import cats.effect.*
import cats.syntax.all.*
import scala.jdk.CollectionConverters.*
import java.io.OutputStream

trait ChildProcess[F[_]]:
def stdin: fs2.Pipe[F, Byte, Unit]
def stdout: Stream[F, Byte]
def stderr: Stream[F, Byte]

object ChildProcess:

def spawn[F[_]: Async](command: String*): Stream[F, ChildProcess[F]] =
Stream.bracket(start[F](command))(_._2).map(_._1)

val readBufferSize = 512
private def start[F[_]: Async](command: Seq[String]) =
Async[F].interruptible {
val p =
new java.lang.ProcessBuilder(command.asJava)
.start() // .directory(new java.io.File(wd)).start()
val done = Async[F].fromCompletableFuture(Sync[F].delay(p.onExit()))

val terminate: F[Unit] = Sync[F].interruptible(p.destroy())

import cats.*
val onGlobal = new (F ~> F):
def apply[A](fa: F[A]): F[A] =
Async[F].evalOn(fa, scala.concurrent.ExecutionContext.global)

val cp = new ChildProcess[F]:
def stdin: fs2.Pipe[F, Byte, Unit] =
writeOutputStreamFlushingChunks[F](
Sync[F].interruptible(p.getOutputStream())
)

def stdout: fs2.Stream[F, Byte] = fs2.io
.readInputStream[F](
Sync[F].interruptible(p.getInputStream()),
chunkSize = readBufferSize
)
.translate(onGlobal)

def stderr: fs2.Stream[F, Byte] = fs2.io
.readInputStream[F](
Sync[F].blocking(p.getErrorStream()),
chunkSize = readBufferSize
)
.translate(onGlobal)
// Avoids broken pipe - we cut off when the program ends.
// Users can decide what to do with the error logs using the exitCode value
.interruptWhen(done.void.attempt)
(cp, terminate)
}

/** Adds a flush after each chunk
*/
def writeOutputStreamFlushingChunks[F[_]](
fos: F[OutputStream],
closeAfterUse: Boolean = true
)(implicit F: Sync[F]): fs2.Pipe[F, Byte, Nothing] =
s =>
def useOs(os: OutputStream): Stream[F, Nothing] =
s.chunks.foreach(c =>
F.interruptible(os.write(c.toArray)) >> F.blocking(os.flush())
)

val os =
if closeAfterUse then Stream.bracket(fos)(os => F.blocking(os.close()))
else Stream.eval(fos)
os.flatMap(os => useOs(os) ++ Stream.exec(F.blocking(os.flush())))
end ChildProcess
32 changes: 32 additions & 0 deletions modules/tracer/backend/src/main/scala/StaticRoutes.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package langoustine.tracer

import org.http4s.*
import cats.effect.*
import org.http4s.dsl.io.*
import java.nio.file.Paths

object Static:
def routes =
val indexHtml = StaticFile
.fromResource[IO](
"assets/index.html",
None,
preferGzipped = true
)
.getOrElseF(NotFound())

HttpRoutes.of[IO] {
case req @ GET -> Root / "assets" / filename
if filename.endsWith(".js") || filename.endsWith(".js.map") =>
StaticFile
.fromResource[IO](
Paths.get("assets", filename).toString,
Some(req),
preferGzipped = true
)
.getOrElseF(NotFound())
case req @ GET -> Root => indexHtml
case req if req.method == GET => indexHtml
}
end routes
end Static
71 changes: 71 additions & 0 deletions modules/tracer/backend/src/main/scala/Tracer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package langoustine
package tracer

import jsonrpclib.fs2.*
import cats.effect.IOApp
import cats.effect.ExitCode
import cats.effect.IO
import fs2.Chunk

import org.http4s.EntityEncoder
import jsonrpclib.Payload
import jsonrpclib.ErrorPayload
import jsonrpclib.CallId

object Tracer extends IOApp:
def run(args: List[String]): IO[ExitCode] =
val in = fs2.io.stdin[IO](512)
val out = fs2.io.stdout[IO]

val tracerArgs = args.takeWhile(_.startsWith("!")).map(_.drop(1))

val restArgs = args.drop(tracerArgs.length)

val argMap = tracerArgs
.grouped(2)
.collect { case k :: v :: Nil =>
k -> v
}
.toMap

val byteTopic =
fs2.Stream.eval(fs2.concurrent.Channel.unbounded[IO, Chunk[Byte]])

byteTopic
.flatMap { inBytes =>
byteTopic.flatMap { outBytes =>
byteTopic.flatMap { errBytes =>
ChildProcess
.spawn[IO](restArgs*)
.flatMap { child =>

val redirectInput =
in.chunks.evalTap(inBytes.send).unchunks.through(child.stdin)

val redirectOutput =
child.stdout.chunks
.evalTap(outBytes.send)
.unchunks
.through(out)

redirectInput
.concurrently(redirectOutput)
.concurrently(child.stderr.chunks.evalTap(errBytes.send))
.concurrently(
TracerServer
.create(
inBytes.stream.unchunks,
outBytes.stream.unchunks,
errBytes.stream.unchunks
)
.run(argMap)
)
}
}
}
}
.compile
.drain
.as(ExitCode(0))
end run
end Tracer
Loading

0 comments on commit 6d4d207

Please sign in to comment.