Skip to content

ChildProcess -> fs2 Processes #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

kubukoz
Copy link
Contributor

@kubukoz kubukoz commented Mar 23, 2025

Quick attempt to replace the homegrown ChildProcess with the fs2.io.process API. It's cross-platform, so with a few more tricks we might have a Native/Node-based tracer backend.

@kubukoz
Copy link
Contributor Author

kubukoz commented Mar 23, 2025

I tried to get a native tracer to link. Here's the diff:

diff --git a/build.sbt b/build.sbt
index e98ee533c..a94525142 100644
--- a/build.sbt
+++ b/build.sbt
@@ -29,18 +29,18 @@ inThisBuild(
 )
 
 val V = new {
-  val scala           = "3.3.3"
+  val scala           = "3.3.5"
   val scribe          = "3.13.1"
   val upickle         = "2.0.0"
   val cats            = "2.10.0"
   val jsonrpclib      = "0.0.7"
-  val fs2             = "3.10.0"
-  val http4s          = "0.23.26"
+  val fs2             = "3.11.0"
+  val http4s          = "0.23.30"
   val laminar         = "0.14.5"
   val decline         = "2.4.1"
   val jsoniter        = "2.20.3"
   val weaver          = "0.8.4"
-  val circe           = "0.14.5"
+  val circe           = "0.14.8"
   val http4sJdkClient = "0.9.1"
   val organizeImports = "0.6.0"
   val fansi           = "0.4.0"
@@ -253,7 +253,8 @@ lazy val tracer = projectMatrix
     libraryDependencies += "org.http4s"   %%% "http4s-dsl"          % V.http4s,
     libraryDependencies += "com.monovore" %%% "decline"             % V.decline,
     libraryDependencies += "com.outr"     %%% "scribe-cats"         % V.scribe,
-    libraryDependencies += "com.indoorvivants.detective" %% "platform" % V.detective,
+    libraryDependencies += "com.indoorvivants.detective" %%% "platform" % V.detective,
+    libraryDependencies += "io.chrisdavenport" %%% "crossplatformioapp" % "0.1.0",
     Compile / doc / sources := Seq.empty,
     // embedding frontend in backend's resources
     Compile / resourceGenerators += {
@@ -289,6 +290,15 @@ lazy val tracer = projectMatrix
     }
   )
   .jvmPlatform(V.scalaVersions)
+  .nativePlatform(
+    V.scalaVersions,
+    Seq(
+      libraryDependencies ++= Seq(
+        "com.armanbilge" %%% "epollcat" % "0.1.6"
+      ),
+      nativeConfig ~= (_.withEmbedResources(true))
+    )
+  )
 
 import org.scalajs.linker.interface.Report
 lazy val frontendJS = tracerFrontend.js(V.scala)
@@ -332,6 +342,7 @@ lazy val tracerShared = projectMatrix
   )
   .jsPlatform(V.scalaVersions)
   .jvmPlatform(V.scalaVersions)
+  .nativePlatform(V.scalaVersions)
 
 val scalafixRules = Seq(
   "OrganizeImports",
@@ -411,14 +422,14 @@ import sbtwelcome.*
 
 logo :=
   raw"""
-    |    _                                       _   _            
-    |   | |                                     | | (_)           
-    |   | |     __ _ _ __   __ _  ___  _   _ ___| |_ _ _ __   ___ 
+    |    _                                       _   _
+    |   | |                                     | | (_)
+    |   | |     __ _ _ __   __ _  ___  _   _ ___| |_ _ _ __   ___
     |   | |    / _` | '_ \ / _` |/ _ \| | | / __| __| | '_ \ / _ \
     |   | |___| (_| | | | | (_| | (_) | |_| \__ \ |_| | | | |  __/
     |   |______\__,_|_| |_|\__, |\___/ \__,_|___/\__|_|_| |_|\___|
-    |                       __/ |                                 
-    |                      |___/                                  
+    |                       __/ |
+    |                      |___/
     |
     |${version.value}
     |
diff --git a/modules/tracer/backend/src/main/scala/main.scala b/modules/tracer/backend/src/main/scala/main.scala
index 4b695430e..96090ea19 100644
--- a/modules/tracer/backend/src/main/scala/main.scala
+++ b/modules/tracer/backend/src/main/scala/main.scala
@@ -33,8 +33,9 @@ import langoustine.lsp.all.ShowMessageParams
 import com.github.plokhotnyuk.jsoniter_scala.core.*
 import fs2.concurrent.Channel
 import jsonrpclib.Message
+import io.chrisdavenport.crossplatformioapp.CrossPlatformIOApp
 
-object LangoustineTracer extends IOApp:
+object LangoustineTracer extends CrossPlatformIOApp:
   def run(args: List[String]): IO[ExitCode] =
     Config.command.parse(args, sys.env) match
       case Left(help) =>
diff --git a/modules/tracer/backend/src/main/scala/routes.static.scala b/modules/tracer/backend/src/main/scala/routes.static.scala
index 374b7273c..f6194ab0c 100644
--- a/modules/tracer/backend/src/main/scala/routes.static.scala
+++ b/modules/tracer/backend/src/main/scala/routes.static.scala
@@ -19,32 +19,49 @@ package langoustine.tracer
 import org.http4s.*
 import cats.effect.*
 import org.http4s.dsl.io.*
-import java.nio.file.Paths
+import fs2.io.file.Path
+import cats.data.OptionT
 
 object Static:
-  def routes =
-    val indexHtml = StaticFile
-      .fromResource[IO](
-        "assets/index.html",
+  def routes: HttpRoutes[IO] =
+    val indexHtml =
+      // StaticFile.
+      fromResource(
+        Path("assets") / "index.html",
         None,
         preferGzipped = true
       )
-      .getOrElseF(NotFound())
+        .getOrElseF(NotFound())
 
     HttpRoutes.of[IO] {
       case req @ GET -> Root / "assets" / filename
           if filename.endsWith(".js") ||
             filename.endsWith(".js.map") ||
             filename.endsWith(".svg") =>
-        StaticFile
-          .fromResource[IO](
-            Paths.get("assets", filename).toString,
-            Some(req),
-            preferGzipped = true
-          )
+        // StaticFile.
+        fromResource(
+          Path("assets") / filename,
+          Some(req),
+          preferGzipped = true
+        )
           .getOrElseF(NotFound())
       case req @ GET -> Root        => indexHtml
       case req if req.method == GET => indexHtml
     }
   end routes
+
+  // https://github.com/http4s/http4s/issues/7648
+  private def fromResource(
+      path: Path,
+      request: Option[Request[IO]],
+      preferGzipped: Boolean = false
+  ): OptionT[IO, Response[IO]] =
+    OptionT.pure {
+      Response(status = Status.Ok).withEntity {
+        fs2.io.readClassResource[IO, Static.type](
+          name = s"/${path.toString}",
+          chunkSize = 4192
+        )
+      }
+    }
 end Static

So basically we need http4s/http4s#7648 (or the local workaround), crossplatformioapp/epollcat, and the only part that was missing for me was the libcrypto library.

Update: well, I got it to link and start locally, and it even prints the port of the server after launching, but unfortunately none of the HTTP APIs work. I suppose it has to do with blocking in stdio streams of the tracer itself, combined with the process run. Given SN 0.4 doesn't support multithreading, I guess we'll have to wait with this dream until 0.5 gets supported in Cats Effect and the rest of the stack... or maybe the polling system in CE will allow fs2.io to work without blocking actual threads?

@@ -107,7 +114,6 @@ def Trace(
.through(inBytes.publish)
.onFinalize(
Logging.info("process stdin finished, shutting down tracer") *>
child.terminate *>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests pass, but I feel like we may be missing something if this isn't called. The fs2 process API doesn't actually have a method for killing the process, guess you have to somehow provoke the resource to exit early instead.

How would I confirm whether this is fine as-is?

@kubukoz kubukoz marked this pull request as ready for review March 23, 2025 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant