Skip to content

Commit

Permalink
Fix tracer and improve frontend devex (#28)
Browse files Browse the repository at this point in the history
* Add basic Tracer tests
* Be more careful about parallel resources
* Frontend filter
* CLI Config and docs
  • Loading branch information
keynmol authored Aug 27, 2022
1 parent 6d4d207 commit 165ed46
Show file tree
Hide file tree
Showing 23 changed files with 1,745 additions and 288 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ jobs:
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}

- name: Build API doc
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
run: sbt --client "lsp/doc"

- name: Publish gh-pages
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ target

project/metals.sbt
website
node_modules
.scala-build
41 changes: 37 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ val V = new {
val fs2 = "3.2.12"
val http4s = "0.23.15"
val laminar = "0.14.2"
val decline = "2.2.0"
}

lazy val noPublishing = Seq(
Expand All @@ -56,6 +57,7 @@ lazy val root = project
.aggregate(lsp.projectRefs*)
.aggregate(tracer.projectRefs*)
.aggregate(tracerShared.projectRefs*)
.aggregate(tracerTests.projectRefs*)
.settings(noPublishing)

lazy val meta = projectMatrix
Expand Down Expand Up @@ -103,27 +105,45 @@ lazy val generate = projectMatrix
lazy val tracer = projectMatrix
.in(file("modules/tracer/backend"))
.dependsOn(lsp, tracerShared)
.enablePlugins(JavaAppPackaging)
.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,
libraryDependencies += "org.http4s" %%% "http4s-ember-server" % V.http4s,
libraryDependencies += "org.http4s" %%% "http4s-dsl" % V.http4s,
libraryDependencies += "com.monovore" %%% "decline" % V.decline,
// embedding frontend in backend's resources
Compile / resourceGenerators += {
Def.task[Seq[File]] {
val (_, location) = (ThisBuild / frontendOutput).value

val indexFile =
(ThisBuild / baseDirectory).value / "modules" / "tracer" / "frontend" / "index.html"
val outDir = (Compile / resourceManaged).value / "assets"

val indexFileContents = {
val lines = IO.readLines(indexFile)

val newLines = lines.collect {
case l if l.contains("<!-- REPLACE -->") =>
"""<script type="text/javascript" src="/assets/main.js"></script>"""
case l => l
}

newLines.mkString(System.lineSeparator())
}

IO.write(outDir / "index.html", indexFileContents)

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

IO.copyFile(file, out)

out
}
} :+ (outDir / "index.html")
}
}
)
Expand Down Expand Up @@ -168,6 +188,19 @@ lazy val tracerShared = projectMatrix
.jsPlatform(scalaVersions)
.jvmPlatform(scalaVersions)

lazy val tracerTests = projectMatrix
.in(file("modules/tracer/tests"))
.defaultAxes(default*)
.dependsOn(tracer)
.settings(
libraryDependencies += "org.http4s" %%% "http4s-ember-client" % V.http4s % Test,
libraryDependencies += "com.disneystreaming" %% "weaver-cats" % "0.7.15" % Test,
libraryDependencies += "org.http4s" %% "http4s-jdk-http-client" % "0.7.0" % Test,
testFrameworks += new TestFramework("weaver.framework.CatsEffect")
)
.jvmPlatform(scalaVersions)
.settings(noPublishing)

lazy val docs = projectMatrix
.in(file("docs"))
.dependsOn(lsp)
Expand Down
72 changes: 72 additions & 0 deletions docs/_docs/tracer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Tracer

![2022-08-27 17 50 05](https://user-images.githubusercontent.com/1052965/187040059-f6e0c08b-7c76-4899-b370-0e8ada0c4819.gif)


A UI tool for capturing the LSP requests and responses that are exchanged between the client and the server.

It doesn't matter what the target LSP is implemented with, as we only intercept JSONRPC payloads sent to stdin and stdout.

Currently, primary method of distribution is via [Coursier](https://get-coursier.io/docs/overview).

To integrate with an LSP of your choosing, you need to have access to the CLI command that launches is.

The principle remains the same regardless of the editor:

```
cs launch tech.neander:langoustine-tracer_3:latest.release -- <lsp command>
```

where `<lsp command>` is passed as a list of arguments, **not as a string**.

### Packaging with coursier

You can make your life considerably easier by using the bootstrap command that Coursier provides:

```
cs boostrap tech.neander:langoustine-tracer_3:latest.release -f -o langoustine-tracer
# now you can use ./langoustine-tracer,
# put it somewhere on your PATH so that it's globally avalable
# and use it like this:
langoustine-tracer --port 9911 my-awesome lsp --stdin true
```

Alternatively, if your system was setup with coursier (i.e. the path where it puts
installed applications is in your system's `PATH`), then life is even easier,
hedonistic even:

```
cs install tech.neander:langoustine-tracer_3:latest.release
```

**From this point in the document we assume that you somehow installed this app in a global location**

For example, if your command to launch the LSP is `my-awesome lsp --stdin true`, then to trace it you need to use the following command:

```
langoustine-tracer my-awesome lsp --stdin true
```

Tracer will interpret everything after the **first `--`** as the command that launches your LSP.
Everything **before first `--`** will be used to configure the tracer itself.

## Tips

### Changing port

By default, it will start the server at the port **9977** but you can change that using the `--port` argument:

```
langoustine-tracer --port 9911 my-awesome lsp --stdin true
```

### Local development

If you are working on the tracer itself, you can globally alias `langoustine-tracer` to a local installation:

```
alias langoustine-tracer=<path-to-langoustine>/modules/tracer/backend/target/jvm-3/universal/stage/bin/langoustine-tracer
```

That launcher is created by running `sbt tracer/stage`
4 changes: 4 additions & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
index: index.md
subsection:
- title: Tracer
page: tracer.md

- title: Examples
page: examples.md

- title: Contributing guide
page: CONTRIBUTING.md
21 changes: 0 additions & 21 deletions modules/tracer/backend/src/main/resources/assets/index.html

This file was deleted.

3 changes: 3 additions & 0 deletions modules/tracer/backend/src/main/scala/ChildProcess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ object ChildProcess:
def spawn[F[_]: Async](command: String*): Stream[F, ChildProcess[F]] =
Stream.bracket(start[F](command))(_._2).map(_._1)

def resource[F[_]: Async](command: String*): Resource[F, ChildProcess[F]] =
Resource.make(start[F](command))(_._2).map(_._1)

val readBufferSize = 512
private def start[F[_]: Async](command: Seq[String]) =
Async[F].interruptible {
Expand Down
28 changes: 28 additions & 0 deletions modules/tracer/backend/src/main/scala/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package langoustine.tracer

import cats.implicits.*
import com.monovore.decline.*
import cats.data.NonEmptyList

case class Config(port: Int, cmd: NonEmptyList[String])

object Config:

val portOpt =
val portHelo = "Port to start Tracer on"
Opts
.option[Int]("port", portHelo)
.orElse(Opts.env[Int]("PORT", portHelo))
.withDefault(9977)

val lsp = Opts.arguments[String]("lspCommand")

val config = (portOpt, lsp).mapN(Config.apply)

val command = Command(
name = "tracer",
header = "Launch Langoustine Tracer"
) {
config
}
end Config
103 changes: 54 additions & 49 deletions modules/tracer/backend/src/main/scala/Tracer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package langoustine
package tracer

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

import org.http4s.EntityEncoder
Expand All @@ -14,58 +13,64 @@ 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]
Config.command.parse(args, sys.env) match
case Left(help) =>
if help.errors.isEmpty then
IO.consoleForIO.errorln(help).as(ExitCode.Success)
else IO.consoleForIO.errorln(help).as(ExitCode.Error)

val tracerArgs = args.takeWhile(_.startsWith("!")).map(_.drop(1))
case Right(c) =>
Launch(c)

val restArgs = args.drop(tracerArgs.length)
end run
end Tracer

val argMap = tracerArgs
.grouped(2)
.collect { case k :: v :: Nil =>
k -> v
}
.toMap
def Launch(config: Config) =
val byteTopic =
Resource.eval(fs2.concurrent.Channel.synchronous[IO, Chunk[Byte]])

val byteTopic =
fs2.Stream.eval(fs2.concurrent.Channel.unbounded[IO, Chunk[Byte]])
val in = fs2.io.stdin[IO](512)
val out = fs2.io.stdout[IO]

byteTopic
.flatMap { inBytes =>
byteTopic.flatMap { outBytes =>
byteTopic.flatMap { errBytes =>
ChildProcess
.spawn[IO](restArgs*)
.flatMap { child =>
(byteTopic, byteTopic, byteTopic).parTupled
.flatMap { case (inBytes, outBytes, errBytes) =>
ChildProcess.resource[IO](config.cmd.toList*).flatMap { child =>
val redirectInput =
in.chunks
.evalTap(inBytes.send)
.unchunks
.through(child.stdin)
.compile
.drain
.background

val redirectInput =
in.chunks.evalTap(inBytes.send).unchunks.through(child.stdin)
val redirectOutput =
child.stdout.chunks
.evalTap(outBytes.send)
.unchunks
.through(out)
.compile
.drain
.background

val redirectOutput =
child.stdout.chunks
.evalTap(outBytes.send)
.unchunks
.through(out)
val redirectLogs =
child.stderr.chunks
.evalTap(errBytes.send)
.compile
.drain
.background

redirectInput
.concurrently(redirectOutput)
.concurrently(child.stderr.chunks.evalTap(errBytes.send))
.concurrently(
TracerServer
.create(
inBytes.stream.unchunks,
outBytes.stream.unchunks,
errBytes.stream.unchunks
)
.run(argMap)
)
}
}
}
val server = TracerServer
.create(
inBytes.stream.unchunks,
outBytes.stream.unchunks,
errBytes.stream.unchunks
)
.runResource(config)

(redirectInput, redirectOutput, redirectLogs, server).parTupled
}
.compile
.drain
.as(ExitCode(0))
end run
end Tracer
}
.useForever
.as(ExitCode.Success)
end Launch
Loading

0 comments on commit 165ed46

Please sign in to comment.