From 9404fd169b974335749b55e3db99d18ba75a16c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 23 Mar 2025 01:36:21 +0100 Subject: [PATCH 1/3] ChildProcess -> fs2 Processes --- .../src/test/scala/EndToEndTests.scala | 13 ++- .../backend/src/main/scala/ChildProcess.scala | 101 ------------------ .../src/main/scala/command.trace.scala | 12 ++- 3 files changed, 20 insertions(+), 106 deletions(-) delete mode 100644 modules/tracer/backend/src/main/scala/ChildProcess.scala diff --git a/modules/e2e-tests/src/test/scala/EndToEndTests.scala b/modules/e2e-tests/src/test/scala/EndToEndTests.scala index a7ec3a434..6ffd7c9d3 100644 --- a/modules/e2e-tests/src/test/scala/EndToEndTests.scala +++ b/modules/e2e-tests/src/test/scala/EndToEndTests.scala @@ -21,6 +21,7 @@ import cats.effect.kernel.Resource import java.lang.management.ManagementFactory import langoustine.example.{MyCustomRequest, MyCustomNotification} +import fs2.io.process.Processes case class Result(code: Int, stdout: List[String], stderr: List[String]) @@ -91,7 +92,11 @@ object EndToEndTests extends SimpleIOSuite: .compile .toVector - langoustine.ChildProcess.resource[IO](process*).use { prc => + val procR = Processes[IO].spawn( + fs2.io.process.ProcessBuilder(process.head, process.tail*) + ) + + procR.use { prc => IO.ref("") .flatMap { fromStdout => prc.stdout @@ -100,7 +105,11 @@ object EndToEndTests extends SimpleIOSuite: .compile .drain .start *> - fs2.Stream.emits(send.getBytes).through(prc.stdin).compile.drain *> + fs2.Stream + .emits(send.getBytes) + .through(prc.stdin) + .compile + .drain *> IO.sleep(timeout) *> fromStdout.get } diff --git a/modules/tracer/backend/src/main/scala/ChildProcess.scala b/modules/tracer/backend/src/main/scala/ChildProcess.scala deleted file mode 100644 index ca5445969..000000000 --- a/modules/tracer/backend/src/main/scala/ChildProcess.scala +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2022 Neandertech - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package langoustine - -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] - def terminate: F[Unit] - -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] - ): F[(ChildProcess[F], F[Unit])] = - 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 terminator: 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 terminate = terminator - def stdin: fs2.Pipe[F, Byte, Unit] = - writeOutputStreamFlushingChunks[F]( - Sync[F].interruptible(p.getOutputStream()), - closeAfterUse = true - ) - - 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, terminator) - } - - /** 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 diff --git a/modules/tracer/backend/src/main/scala/command.trace.scala b/modules/tracer/backend/src/main/scala/command.trace.scala index b78e85d36..7e26e0658 100644 --- a/modules/tracer/backend/src/main/scala/command.trace.scala +++ b/modules/tracer/backend/src/main/scala/command.trace.scala @@ -19,6 +19,7 @@ import org.http4s.server.Server import jsonrpclib.fs2.lsp import scala.util.NotGiven import jsonrpclib.Message +import fs2.io.process.Processes extension (s: fs2.Stream[IO, Payload]) def debugAs(name: String) = @@ -50,8 +51,14 @@ def Trace( summary: Summary ) = fs2.Stream.eval(IO.deferred[Communicate[IO]]).flatMap { comms => - langoustine.ChildProcess - .spawn[IO](traceConfig.cmd.toList*) + fs2.Stream + .resource { + Processes[IO] + .spawn( + fs2.io.process + .ProcessBuilder(traceConfig.cmd.head, traceConfig.cmd.tail) + ) + } .flatMap { child => import langoustine.lsp.jsonrpcIntegration.given @@ -107,7 +114,6 @@ def Trace( .through(inBytes.publish) .onFinalize( Logging.info("process stdin finished, shutting down tracer") *> - child.terminate *> exits.complete(true).void ) From 3be6a7ea759fb7d864bc6d0a4a1ccc63b6d5bb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 23 Mar 2025 02:01:39 +0100 Subject: [PATCH 2/3] Replace another process API case --- modules/tracer/backend/src/main/scala/browser.open.scala | 9 ++++++--- .../tracer/backend/src/main/scala/tracer.endpoints.scala | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 modules/tracer/backend/src/main/scala/tracer.endpoints.scala diff --git a/modules/tracer/backend/src/main/scala/browser.open.scala b/modules/tracer/backend/src/main/scala/browser.open.scala index d2e519566..603907dcc 100644 --- a/modules/tracer/backend/src/main/scala/browser.open.scala +++ b/modules/tracer/backend/src/main/scala/browser.open.scala @@ -9,12 +9,15 @@ import com.indoorvivants.detective.Platform.OS.MacOS import com.indoorvivants.detective.Platform.OS.Linux import com.indoorvivants.detective.Platform.OS.Unknown import scala.util.Failure +import fs2.io.process.Processes def openBrowser(url: Uri): IO[Unit] = val stringUri = url.renderString def cmd(args: String*) = - Try(sys.process.Process.apply(args).!!) + Processes[IO] + .spawn(fs2.io.process.ProcessBuilder(args.head, args.tail*)) + .use(_.stdout.compile.drain) val mac = cmd("open", stringUri) val linux = @@ -24,8 +27,8 @@ def openBrowser(url: Uri): IO[Unit] = Platform.os match case Windows => Logging.error("Cannot open the browser on windows") - case MacOS => IO.fromTry(mac).void - case Linux => IO.fromTry(linux).void + case MacOS => mac.void + case Linux => linux.void case Unknown => Logging.error("Cannot open the browser on unknown platform") end openBrowser diff --git a/modules/tracer/backend/src/main/scala/tracer.endpoints.scala b/modules/tracer/backend/src/main/scala/tracer.endpoints.scala deleted file mode 100644 index c4d8d3ab3..000000000 --- a/modules/tracer/backend/src/main/scala/tracer.endpoints.scala +++ /dev/null @@ -1 +0,0 @@ -package langoustine.tracer From 5fc188234ad08dddff22a801b0ca89bc48bb01b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 23 Mar 2025 02:24:02 +0100 Subject: [PATCH 3/3] fix ci --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3aba6bb99..c9fefc88e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,12 @@ jobs: - uses: actions/setup-java@v3 with: - distribution: 'temurin' - java-version: '17' - cache: 'sbt' + distribution: "temurin" + java-version: "17" + cache: "sbt" + + - name: Setup sbt + uses: sbt/setup-sbt@v1 - name: Test run: sbt ci buildWebsite