From d57526c8b07f468f611cf6f0f7588a912574d0f4 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sat, 8 Mar 2025 16:16:08 +0530 Subject: [PATCH 01/18] Added output configurations --- .../scala/fs2/io/process/ProcessBuilder.scala | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala index 7ab5a19eeb..98de2da6d3 100644 --- a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala +++ b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala @@ -1,24 +1,3 @@ -/* - * Copyright (c) 2013 Functional Streams for Scala - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - package fs2.io package process @@ -49,6 +28,9 @@ sealed abstract class ProcessBuilder private { */ def workingDirectory: Option[Path] + /** Configures how stdout and stderr should be handled. */ + def outputMode: ProcessOutputMode + /** @see [[command]] */ def withCommand(command: String): ProcessBuilder @@ -67,6 +49,9 @@ sealed abstract class ProcessBuilder private { /** @see [[workingDirectory]] */ def withCurrentWorkingDirectory: ProcessBuilder + /** @see [[outputMode]] */ + def withOutputMode(outputMode: ProcessOutputMode): ProcessBuilder + /** Starts the process and returns a handle for interacting with it. * Closing the resource will kill the process if it has not already terminated. */ @@ -74,10 +59,19 @@ sealed abstract class ProcessBuilder private { Processes[F].spawn(this) } +sealed trait ProcessOutputMode +object ProcessOutputMode { + case object Separate extends ProcessOutputMode // stdout and stderr are separate + case object Merged extends ProcessOutputMode // stderr is redirected to stdout + case class FileOutput(path: Path) extends ProcessOutputMode // Output to file + case object Inherit extends ProcessOutputMode // Inherit parent process's streams + case object Ignore extends ProcessOutputMode // Discard output +} + object ProcessBuilder { def apply(command: String, args: List[String]): ProcessBuilder = - ProcessBuilderImpl(command, args, true, Map.empty, None) + ProcessBuilderImpl(command, args, true, Map.empty, None, ProcessOutputMode.Separate) def apply(command: String, args: String*): ProcessBuilder = apply(command, args.toList) @@ -87,7 +81,8 @@ object ProcessBuilder { args: List[String], inheritEnv: Boolean, extraEnv: Map[String, String], - workingDirectory: Option[Path] + workingDirectory: Option[Path], + outputMode: ProcessOutputMode ) extends ProcessBuilder { def withCommand(command: String): ProcessBuilder = copy(command = command) @@ -100,7 +95,9 @@ object ProcessBuilder { def withWorkingDirectory(workingDirectory: Path): ProcessBuilder = copy(workingDirectory = Some(workingDirectory)) + def withCurrentWorkingDirectory: ProcessBuilder = copy(workingDirectory = None) - } + def withOutputMode(outputMode: ProcessOutputMode): ProcessBuilder = copy(outputMode = outputMode) + } } From 196feb95fc538656d84010bdf3b64636354b1a6e Mon Sep 17 00:00:00 2001 From: Sauren Sharma <87110386+Dralt03@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:20:58 +0530 Subject: [PATCH 02/18] Update ProcessBuilder.scala --- .../scala/fs2/io/process/ProcessBuilder.scala | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala index 98de2da6d3..37771cf8b5 100644 --- a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala +++ b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala @@ -1,3 +1,24 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + package fs2.io package process From 23b3a7dc51987a7bce34e4245690140a805a9090 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Tue, 11 Mar 2025 21:53:33 +0530 Subject: [PATCH 03/18] Added implementation for Native and JS --- .../fs2/io/process/ProcessesPlatform.scala | 29 +++-- .../fs2/io/process/ProcessesPlatform.scala | 101 ++++++++++-------- .../main/scala/fs2/io/process/Processes.scala | 1 - 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index dc54026941..d240a5fe45 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -36,19 +36,32 @@ private[process] trait ProcessesCompanionPlatform { def spawn(process: ProcessBuilder): Resource[F, Process[F]] = Resource { F.async_[(Process[F], F[Unit])] { cb => - val childProcess = facade.child_process.spawn( - process.command, - process.args.toJSArray, - new facade.child_process.SpawnOptions { - cwd = process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) - env = + + + val spawnOptions = js.Dynamic.literal { + "cwd" -> process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) + "env" ->( if (process.inheritEnv) (facade.process.env ++ process.extraEnv).toJSDictionary else - process.extraEnv.toJSDictionary + process.extraEnv.toJSDictionary) } + + val childProcess = facade.child_process.spawn( + process.command, + process.args.toJSArray, + spawnOptions.asInstanceOf[facade.child_process.SpawnOptions] ) + process.outputMode match { + case ProcessOutputMode.Separate => // Default behavior + case ProcessOutputMode.Merged => spawnOptions.updateDynamic("stdio")("pipe") + case ProcessOutputMode.FileOutput(path) => + spawnOptions.updateDynamic("stdio")(js.Array("pipe", path.toString, path.toString)) + case ProcessOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") + case ProcessOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") + } + val fs2Process = new UnsealedProcess[F] { def isAlive: F[Boolean] = F.delay { @@ -84,7 +97,7 @@ private[process] trait ProcessesCompanionPlatform { } else { childProcess.kill() childProcess.once("exit", () => cb(Either.unit)) - Left(None) + Left(None) } } } diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 315c09847d..ec2cc37ef0 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -27,8 +27,7 @@ import cats.effect.kernel.Async import cats.effect.kernel.Resource import cats.syntax.all._ import fs2.io.CollectionCompat._ - -import java.lang +import java.lang.ProcessBuilder.Redirect private[process] trait ProcessesCompanionPlatform { def forAsync[F[_]](implicit F: Async[F]): Processes[F] = new UnsealedProcesses[F] { @@ -36,58 +35,70 @@ private[process] trait ProcessesCompanionPlatform { Resource .make { F.blocking { - val builder = new lang.ProcessBuilder((process.command :: process.args).asJava) + val builder = new java.lang.ProcessBuilder((process.command :: process.args).asJava) - process.workingDirectory.foreach { path => - builder.directory(path.toNioPath.toFile) - } + process.workingDirectory.foreach { path => + builder.directory(path.toNioPath.toFile) + } - val env = builder.environment() - if (!process.inheritEnv) env.clear() - process.extraEnv.foreach { case (k, v) => - env.put(k, v) - } + val env = builder.environment() + if (!process.inheritEnv) env.clear() + process.extraEnv.foreach { case (k, v) => + env.put(k, v) + } - builder.start() + process.outputMode match { + case ProcessOutputMode.Separate => // Default behavior + case ProcessOutputMode.Merged => builder.redirectErrorStream(true) + case ProcessOutputMode.FileOutput(path) => + builder.redirectOutput(Redirect.to(path.toNioPath.toFile)) + case ProcessOutputMode.Inherit => + builder.redirectOutput(Redirect.INHERIT) + builder.redirectError(Redirect.INHERIT) + case ProcessOutputMode.Ignore => + builder.redirectOutput(Redirect.DISCARD) + builder.redirectError(Redirect.DISCARD) } - } { process => - F.delay(process.isAlive()) - .ifM( - F.blocking { - process.destroy() - process.waitFor() - () - }, - F.unit - ) - } - .map { process => - new UnsealedProcess[F] { - def isAlive = F.delay(process.isAlive()) - def exitValue = isAlive.ifM( - F.interruptible(process.waitFor()), - F.delay(process.exitValue()) - ) + builder.start() + } + } { process => + F.delay(process.isAlive()) + .ifM( + F.blocking { + process.destroy() + process.waitFor() + () + }, + F.unit + ) + } + .map { process => + new UnsealedProcess[F] { + def isAlive = F.delay(process.isAlive()) - def stdin = writeOutputStreamCancelable( - F.delay(process.getOutputStream()), - F.blocking(process.destroy()) - ) + def exitValue = isAlive.ifM( + F.interruptible(process.waitFor()), + F.delay(process.exitValue()) + ) - def stdout = readInputStreamCancelable( - F.delay(process.getInputStream()), - F.blocking(process.destroy()), - 8192 - ) + def stdin = writeOutputStreamCancelable( + F.delay(process.getOutputStream()), + F.blocking(process.destroy()) + ) - def stderr = readInputStreamCancelable( - F.delay(process.getErrorStream()), - F.blocking(process.destroy()), - 8192 - ) + def stdout = readInputStreamCancelable( + F.delay(process.getInputStream()), + F.blocking(process.destroy()), + 8192 + ) - } + def stderr = readInputStreamCancelable( + F.delay(process.getErrorStream()), + F.blocking(process.destroy()), + 8192 + ) + } } } } diff --git a/io/shared/src/main/scala/fs2/io/process/Processes.scala b/io/shared/src/main/scala/fs2/io/process/Processes.scala index ddca581653..235174c2ad 100644 --- a/io/shared/src/main/scala/fs2/io/process/Processes.scala +++ b/io/shared/src/main/scala/fs2/io/process/Processes.scala @@ -25,7 +25,6 @@ import cats.effect.IO import cats.effect.LiftIO import cats.effect.kernel.Async import cats.effect.kernel.Resource - sealed trait Processes[F[_]] { def spawn(process: ProcessBuilder): Resource[F, Process[F]] From caef85d8dd39f9b9cdc03dbf9d958aaa634a559b Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Wed, 12 Mar 2025 22:41:03 +0530 Subject: [PATCH 04/18] Resolved Conflicts --- .../fs2/io/process/ProcessesPlatform.scala | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index ec2cc37ef0..673ddc4bb3 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -25,8 +25,10 @@ package process import cats.effect.kernel.Async import cats.effect.kernel.Resource -import cats.syntax.all._ -import fs2.io.CollectionCompat._ +import cats.syntax.all.* +import fs2.io.CollectionCompat.* + +import java.lang import java.lang.ProcessBuilder.Redirect private[process] trait ProcessesCompanionPlatform { @@ -63,19 +65,26 @@ private[process] trait ProcessesCompanionPlatform { builder.start() } } { process => - F.delay(process.isAlive()) - .ifM( - F.blocking { - process.destroy() - process.waitFor() - () - }, - F.unit - ) - } - .map { process => - new UnsealedProcess[F] { - def isAlive = F.delay(process.isAlive()) + F.delay(process.isAlive()) + .ifM( + evalOnVirtualThreadIfAvailable( + F.blocking { + process.destroy() + process.waitFor() + () + } + ), + F.unit + ) + } + .map { process => + new UnsealedProcess[F] { + def isAlive = F.delay(process.isAlive()) + + def exitValue = isAlive.ifM( + evalOnVirtualThreadIfAvailable(F.interruptible(process.waitFor())), + F.delay(process.exitValue()) + ) def exitValue = isAlive.ifM( F.interruptible(process.waitFor()), From 4d279b2613d03c0f5da785582ffe0012f53cfcc0 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Thu, 13 Mar 2025 11:05:44 +0530 Subject: [PATCH 05/18] Added seperate Streams for stdin, stdout and stderr --- .../fs2/io/process/ProcessesPlatform.scala | 29 +++++++++++---- .../fs2/io/process/ProcessesPlatform.scala | 35 ++++++++++++------- .../scala/fs2/io/process/ProcessBuilder.scala | 29 ++++++++------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index d240a5fe45..280360343f 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -53,13 +53,28 @@ private[process] trait ProcessesCompanionPlatform { spawnOptions.asInstanceOf[facade.child_process.SpawnOptions] ) - process.outputMode match { - case ProcessOutputMode.Separate => // Default behavior - case ProcessOutputMode.Merged => spawnOptions.updateDynamic("stdio")("pipe") - case ProcessOutputMode.FileOutput(path) => - spawnOptions.updateDynamic("stdio")(js.Array("pipe", path.toString, path.toString)) - case ProcessOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") - case ProcessOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") + process.outputConfig.stdin match { + case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") + case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") + case StreamOutputMode.FileOutput(path) => + spawnOptions.updateDynamic("stdio")(js.Array(path.toString, "pipe", "pipe")) + case StreamOutputMode.Pipe => + } + + process.outputConfig.stdout match { + case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") + case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") + case StreamOutputMode.FileOutput(path) => + spawnOptions.updateDynamic("stdio")(js.Array("pipe", path.toString, "pipe")) + case StreamOutputMode.Pipe => + } + + process.outputConfig.stderr match { + case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") + case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") + case StreamOutputMode.FileOutput(path) => + spawnOptions.updateDynamic("stdio")(js.Array("pipe", "pipe", path.toString)) + case StreamOutputMode.Pipe => } val fs2Process = new UnsealedProcess[F] { diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index fcb00426a4..e3331a2658 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -51,18 +51,29 @@ private[process] trait ProcessesCompanionPlatform { env.put(k, v) } - process.outputMode match { - case ProcessOutputMode.Separate => // Default behavior - case ProcessOutputMode.Merged => builder.redirectErrorStream(true) - case ProcessOutputMode.FileOutput(path) => - builder.redirectOutput(Redirect.to(path.toNioPath.toFile)) - case ProcessOutputMode.Inherit => - builder.redirectOutput(Redirect.INHERIT) - builder.redirectError(Redirect.INHERIT) - case ProcessOutputMode.Ignore => - builder.redirectOutput(Redirect.DISCARD) - builder.redirectError(Redirect.DISCARD) - } + process.outputConfig.stdin match { + case StreamOutputMode.Inherit => builder.redirectInput(Redirect.INHERIT) + case StreamOutputMode.Ignore => builder.redirectInput(Redirect.DISCARD) + case StreamOutputMode.FileOutput(path) => + builder.redirectInput(Redirect.from(path.toNioPath.toFile)) + case StreamOutputMode.Pipe => + } + + process.outputConfig.stdout match { + case StreamOutputMode.Inherit => builder.redirectOutput(Redirect.INHERIT) + case StreamOutputMode.Ignore => builder.redirectOutput(Redirect.DISCARD) + case StreamOutputMode.FileOutput(path) => + builder.redirectOutput(Redirect.to(path.toNioPath.toFile)) + case StreamOutputMode.Pipe => + } + + process.outputConfig.stderr match { + case StreamOutputMode.Inherit => builder.redirectError(Redirect.INHERIT) + case StreamOutputMode.Ignore => builder.redirectError(Redirect.DISCARD) + case StreamOutputMode.FileOutput(path) => + builder.redirectError(Redirect.to(path.toNioPath.toFile)) + case StreamOutputMode.Pipe => + } builder.start() } diff --git a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala index 37771cf8b5..01d9fc5ebe 100644 --- a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala +++ b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala @@ -50,7 +50,7 @@ sealed abstract class ProcessBuilder private { def workingDirectory: Option[Path] /** Configures how stdout and stderr should be handled. */ - def outputMode: ProcessOutputMode + def outputConfig: ProcessOutputConfig /** @see [[command]] */ def withCommand(command: String): ProcessBuilder @@ -71,7 +71,7 @@ sealed abstract class ProcessBuilder private { def withCurrentWorkingDirectory: ProcessBuilder /** @see [[outputMode]] */ - def withOutputMode(outputMode: ProcessOutputMode): ProcessBuilder + def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder /** Starts the process and returns a handle for interacting with it. * Closing the resource will kill the process if it has not already terminated. @@ -80,19 +80,24 @@ sealed abstract class ProcessBuilder private { Processes[F].spawn(this) } -sealed trait ProcessOutputMode -object ProcessOutputMode { - case object Separate extends ProcessOutputMode // stdout and stderr are separate - case object Merged extends ProcessOutputMode // stderr is redirected to stdout - case class FileOutput(path: Path) extends ProcessOutputMode // Output to file - case object Inherit extends ProcessOutputMode // Inherit parent process's streams - case object Ignore extends ProcessOutputMode // Discard output +sealed trait StreamOutputMode +object StreamOutputMode { + case object Pipe extends StreamOutputMode + case object Inherit extends StreamOutputMode + case object Ignore extends StreamOutputMode + case class FileOutput(path: Path) extends StreamOutputMode } +final case class ProcessOutputConfig( + stdin: StreamOutputMode = StreamOutputMode.Pipe, + stdout: StreamOutputMode = StreamOutputMode.Pipe, + stderr: StreamOutputMode = StreamOutputMode.Pipe +) + object ProcessBuilder { def apply(command: String, args: List[String]): ProcessBuilder = - ProcessBuilderImpl(command, args, true, Map.empty, None, ProcessOutputMode.Separate) + ProcessBuilderImpl(command, args, true, Map.empty, None, ProcessOutputConfig()) def apply(command: String, args: String*): ProcessBuilder = apply(command, args.toList) @@ -103,7 +108,7 @@ object ProcessBuilder { inheritEnv: Boolean, extraEnv: Map[String, String], workingDirectory: Option[Path], - outputMode: ProcessOutputMode + outputConfig: ProcessOutputConfig ) extends ProcessBuilder { def withCommand(command: String): ProcessBuilder = copy(command = command) @@ -119,6 +124,6 @@ object ProcessBuilder { def withCurrentWorkingDirectory: ProcessBuilder = copy(workingDirectory = None) - def withOutputMode(outputMode: ProcessOutputMode): ProcessBuilder = copy(outputMode = outputMode) + def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder = copy(outputConfig = outputConfig) } } From 6e17b140d627813791d1735a55121ca5940194a0 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Fri, 14 Mar 2025 17:34:21 +0530 Subject: [PATCH 06/18] Added Test Cases (except Inherit) --- .../fs2/io/process/ProcessesPlatform.scala | 1 - .../scala/fs2/io/process/ProcessSuite.scala | 51 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index e3331a2658..5e5613677f 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -28,7 +28,6 @@ import cats.effect.kernel.Resource import cats.syntax.all.* import fs2.io.CollectionCompat.* -import java.lang import java.lang.ProcessBuilder.Redirect diff --git a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala index 6bd25a2ba7..12b3ac45a6 100644 --- a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala +++ b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala @@ -48,11 +48,9 @@ class ProcessSuite extends Fs2IoSuite { } test("stdout and stderr") { - ProcessBuilder( - "node", - "-e", - "console.log('good day stdout'); console.error('how do you do stderr')" - ).spawn[IO] + ProcessBuilder("node", "-e", "console.log('good day stdout'); console.error('how do you do stderr')") + .withOutputConfig(ProcessOutputConfig()) + .spawn[IO] .use { p => val testOut = p.stdout .through(fs2.text.utf8.decode) @@ -72,6 +70,49 @@ class ProcessSuite extends Fs2IoSuite { } } + test("merged stdout and stderr") { + ProcessBuilder("node", "-e", "console.log('merged stdout'); console.error('merged stderr')") + .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.Pipe, stderr = StreamOutputMode.Pipe)) + .spawn[IO] + .use { p => + p.stdout + .through(fs2.text.utf8.decode) + .compile + .string + .assert(s => s.contains("merged stdout") && s.contains("merged stderr")) + } + } + + test("file output") { + Files[IO].tempFile.use { path => + ProcessBuilder("echo", "file output test") + .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.FileOutput(path))) + .spawn[IO] + .use(_.exitValue) + .assertEquals(0) *> + Files[IO].readUtf8(path).compile.string.assertEquals("file output test\n") + } + } + + test("ignored output") { + ProcessBuilder("echo", "ignored output") + .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.Ignore)) + .spawn[IO] + .use(_.exitValue) + .assertEquals(0) + } + + test("stdin piping") { + ProcessBuilder("cat") + .withOutputConfig(ProcessOutputConfig(stdin = StreamOutputMode.Pipe)) + .spawn[IO] + .use { p => + val input = Stream.emit("piped input test").through(fs2.text.utf8.encode).through(p.stdin).compile.drain + val output = p.stdout.through(fs2.text.utf8.decode).compile.string.assertEquals("piped input test") + input *> output + } + } + if (!isNative) test("cat") { ProcessBuilder("cat").spawn[IO].use { p => From 8fd44731090c2d4e641b2e10076a63ceeaf96086 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Tue, 18 Mar 2025 14:49:40 +0530 Subject: [PATCH 07/18] Added sperate Config methods --- .../io/internal/facade/child_process.scala | 1 + .../fs2/io/process/ProcessesPlatform.scala | 32 +++++++++---------- .../fs2/io/process/ProcessesPlatform.scala | 28 ++++++++-------- .../scala/fs2/io/process/ProcessBuilder.scala | 29 +++++++++++------ 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/internal/facade/child_process.scala b/io/js/src/main/scala/fs2/io/internal/facade/child_process.scala index a719b0345f..659aeb3320 100644 --- a/io/js/src/main/scala/fs2/io/internal/facade/child_process.scala +++ b/io/js/src/main/scala/fs2/io/internal/facade/child_process.scala @@ -46,6 +46,7 @@ private[io] object child_process { var env: js.UndefOr[js.Dictionary[String]] = js.undefined + var stdio: js.UndefOr[js.Any] = js.undefined } @js.native diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 280360343f..c106b0e87e 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -38,7 +38,7 @@ private[process] trait ProcessesCompanionPlatform { F.async_[(Process[F], F[Unit])] { cb => - val spawnOptions = js.Dynamic.literal { + val spawnOptions = new facade.child_process.SpawnOptions { "cwd" -> process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) "env" ->( if (process.inheritEnv) @@ -54,27 +54,27 @@ private[process] trait ProcessesCompanionPlatform { ) process.outputConfig.stdin match { - case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") - case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") - case StreamOutputMode.FileOutput(path) => - spawnOptions.updateDynamic("stdio")(js.Array(path.toString, "pipe", "pipe")) - case StreamOutputMode.Pipe => + case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Pipe => //Default behaviour } process.outputConfig.stdout match { - case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") - case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") - case StreamOutputMode.FileOutput(path) => - spawnOptions.updateDynamic("stdio")(js.Array("pipe", path.toString, "pipe")) - case StreamOutputMode.Pipe => + case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Pipe => //Default behaviour } process.outputConfig.stderr match { - case StreamOutputMode.Inherit => spawnOptions.updateDynamic("stdio")("inherit") - case StreamOutputMode.Ignore => spawnOptions.updateDynamic("stdio")("ignore") - case StreamOutputMode.FileOutput(path) => - spawnOptions.updateDynamic("stdio")(js.Array("pipe", "pipe", path.toString)) - case StreamOutputMode.Pipe => + case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Pipe => //Default behaviour } val fs2Process = new UnsealedProcess[F] { diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 5e5613677f..b72152842b 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -51,27 +51,27 @@ private[process] trait ProcessesCompanionPlatform { } process.outputConfig.stdin match { - case StreamOutputMode.Inherit => builder.redirectInput(Redirect.INHERIT) - case StreamOutputMode.Ignore => builder.redirectInput(Redirect.DISCARD) - case StreamOutputMode.FileOutput(path) => + case StreamRedirect.Inherit => builder.redirectInput(Redirect.INHERIT) + case StreamRedirect.Discard => builder.redirectInput(Redirect.DISCARD) + case StreamRedirect.File(path) => builder.redirectInput(Redirect.from(path.toNioPath.toFile)) - case StreamOutputMode.Pipe => + case StreamRedirect.Pipe => } process.outputConfig.stdout match { - case StreamOutputMode.Inherit => builder.redirectOutput(Redirect.INHERIT) - case StreamOutputMode.Ignore => builder.redirectOutput(Redirect.DISCARD) - case StreamOutputMode.FileOutput(path) => + case StreamRedirect.Inherit => builder.redirectOutput(Redirect.INHERIT) + case StreamRedirect.Discard => builder.redirectOutput(Redirect.DISCARD) + case StreamRedirect.File(path) => builder.redirectOutput(Redirect.to(path.toNioPath.toFile)) - case StreamOutputMode.Pipe => + case StreamRedirect.Pipe => } process.outputConfig.stderr match { - case StreamOutputMode.Inherit => builder.redirectError(Redirect.INHERIT) - case StreamOutputMode.Ignore => builder.redirectError(Redirect.DISCARD) - case StreamOutputMode.FileOutput(path) => + case StreamRedirect.Inherit => builder.redirectError(Redirect.INHERIT) + case StreamRedirect.Discard => builder.redirectError(Redirect.DISCARD) + case StreamRedirect.File(path) => builder.redirectError(Redirect.to(path.toNioPath.toFile)) - case StreamOutputMode.Pipe => + case StreamRedirect.Pipe => } builder.start() @@ -79,11 +79,13 @@ private[process] trait ProcessesCompanionPlatform { } { process => F.delay(process.isAlive()) .ifM( + evalOnVirtualThreadIfAvailable( F.blocking { process.destroy() process.waitFor() () - }, + } + ), F.unit ) } diff --git a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala index 01d9fc5ebe..9546efbacb 100644 --- a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala +++ b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala @@ -73,6 +73,17 @@ sealed abstract class ProcessBuilder private { /** @see [[outputMode]] */ def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder + /* @param mode The mode for handling stdin + */ + def redirectInput(mode: StreamRedirect): ProcessBuilder = + withOutputConfig(outputConfig.copy(stdin = mode)) + + def redirectOutput(mode: StreamRedirect): ProcessBuilder = + withOutputConfig(outputConfig.copy(stdout = mode)) + + def redirectError(mode: StreamRedirect): ProcessBuilder = + withOutputConfig(outputConfig.copy(stderr = mode)) + /** Starts the process and returns a handle for interacting with it. * Closing the resource will kill the process if it has not already terminated. */ @@ -80,18 +91,18 @@ sealed abstract class ProcessBuilder private { Processes[F].spawn(this) } -sealed trait StreamOutputMode -object StreamOutputMode { - case object Pipe extends StreamOutputMode - case object Inherit extends StreamOutputMode - case object Ignore extends StreamOutputMode - case class FileOutput(path: Path) extends StreamOutputMode +sealed abstract class StreamRedirect +object StreamRedirect { + case object Pipe extends StreamRedirect + case object Inherit extends StreamRedirect + case object Discard extends StreamRedirect + final case class File(path: Path) extends StreamRedirect } final case class ProcessOutputConfig( - stdin: StreamOutputMode = StreamOutputMode.Pipe, - stdout: StreamOutputMode = StreamOutputMode.Pipe, - stderr: StreamOutputMode = StreamOutputMode.Pipe + stdin: StreamRedirect = StreamRedirect.Pipe, + stdout: StreamRedirect = StreamRedirect.Pipe, + stderr: StreamRedirect = StreamRedirect.Pipe ) object ProcessBuilder { From a982110ff1b32f608c441c625736061f92606568 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Wed, 19 Mar 2025 21:47:45 +0530 Subject: [PATCH 08/18] Fixed tests for seperate output streams --- .../src/test/scala/fs2/io/process/ProcessSuite.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala index 12b3ac45a6..945aea9e9f 100644 --- a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala +++ b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala @@ -72,7 +72,7 @@ class ProcessSuite extends Fs2IoSuite { test("merged stdout and stderr") { ProcessBuilder("node", "-e", "console.log('merged stdout'); console.error('merged stderr')") - .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.Pipe, stderr = StreamOutputMode.Pipe)) + .withOutputConfig(ProcessOutputConfig(stdout = StreamRedirect.Pipe, stderr = StreamRedirect.Pipe)) .spawn[IO] .use { p => p.stdout @@ -86,7 +86,7 @@ class ProcessSuite extends Fs2IoSuite { test("file output") { Files[IO].tempFile.use { path => ProcessBuilder("echo", "file output test") - .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.FileOutput(path))) + .withOutputConfig(ProcessOutputConfig(stdout = StreamRedirect.File(path))) .spawn[IO] .use(_.exitValue) .assertEquals(0) *> @@ -96,7 +96,7 @@ class ProcessSuite extends Fs2IoSuite { test("ignored output") { ProcessBuilder("echo", "ignored output") - .withOutputConfig(ProcessOutputConfig(stdout = StreamOutputMode.Ignore)) + .withOutputConfig(ProcessOutputConfig(stdout = StreamRedirect.Discard)) .spawn[IO] .use(_.exitValue) .assertEquals(0) @@ -104,7 +104,7 @@ class ProcessSuite extends Fs2IoSuite { test("stdin piping") { ProcessBuilder("cat") - .withOutputConfig(ProcessOutputConfig(stdin = StreamOutputMode.Pipe)) + .withOutputConfig(ProcessOutputConfig(stdin = StreamRedirect.Pipe)) .spawn[IO] .use { p => val input = Stream.emit("piped input test").through(fs2.text.utf8.encode).through(p.stdin).compile.drain From 8a6f34f51051142d32b95c243aa43e2860dda2f4 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Thu, 10 Apr 2025 03:59:04 +0530 Subject: [PATCH 09/18] Passing Tests --- .../fs2/io/process/ProcessesPlatform.scala | 95 +++++++++---------- .../scala/fs2/io/process/ProcessBuilder.scala | 19 ++-- .../scala/fs2/io/process/ProcessSuite.scala | 24 +++-- 3 files changed, 75 insertions(+), 63 deletions(-) diff --git a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala index b72152842b..23a8789f25 100644 --- a/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/jvm-native/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -30,7 +30,6 @@ import fs2.io.CollectionCompat.* import java.lang.ProcessBuilder.Redirect - private[process] trait ProcessesCompanionPlatform { def forAsync[F[_]](implicit F: Async[F]): Processes[F] = new UnsealedProcesses[F] { @@ -38,29 +37,29 @@ private[process] trait ProcessesCompanionPlatform { Resource .make { F.blocking { - val builder = new java.lang.ProcessBuilder((process.command :: process.args).asJava) + val builder = new java.lang.ProcessBuilder((process.command :: process.args).asJava) - process.workingDirectory.foreach { path => - builder.directory(path.toNioPath.toFile) - } + process.workingDirectory.foreach { path => + builder.directory(path.toNioPath.toFile) + } - val env = builder.environment() - if (!process.inheritEnv) env.clear() - process.extraEnv.foreach { case (k, v) => - env.put(k, v) - } + val env = builder.environment() + if (!process.inheritEnv) env.clear() + process.extraEnv.foreach { case (k, v) => + env.put(k, v) + } - process.outputConfig.stdin match { + process.outputConfig.stdin match { case StreamRedirect.Inherit => builder.redirectInput(Redirect.INHERIT) - case StreamRedirect.Discard => builder.redirectInput(Redirect.DISCARD) + case StreamRedirect.Discard => builder.redirectInput(Redirect.DISCARD) case StreamRedirect.File(path) => builder.redirectInput(Redirect.from(path.toNioPath.toFile)) - case StreamRedirect.Pipe => + case StreamRedirect.Pipe => } process.outputConfig.stdout match { case StreamRedirect.Inherit => builder.redirectOutput(Redirect.INHERIT) - case StreamRedirect.Discard => builder.redirectOutput(Redirect.DISCARD) + case StreamRedirect.Discard => builder.redirectOutput(Redirect.DISCARD) case StreamRedirect.File(path) => builder.redirectOutput(Redirect.to(path.toNioPath.toFile)) case StreamRedirect.Pipe => @@ -68,53 +67,53 @@ private[process] trait ProcessesCompanionPlatform { process.outputConfig.stderr match { case StreamRedirect.Inherit => builder.redirectError(Redirect.INHERIT) - case StreamRedirect.Discard => builder.redirectError(Redirect.DISCARD) + case StreamRedirect.Discard => builder.redirectError(Redirect.DISCARD) case StreamRedirect.File(path) => builder.redirectError(Redirect.to(path.toNioPath.toFile)) - case StreamRedirect.Pipe => + case StreamRedirect.Pipe => } - builder.start() + builder.start() + } + } { process => + F.delay(process.isAlive()) + .ifM( + evalOnVirtualThreadIfAvailable( + F.blocking { + process.destroy() + process.waitFor() + () + } + ), + F.unit + ) } - } { process => - F.delay(process.isAlive()) - .ifM( - evalOnVirtualThreadIfAvailable( - F.blocking { - process.destroy() - process.waitFor() - () - } - ), - F.unit - ) - } - .map { process => - new UnsealedProcess[F] { - def isAlive = F.delay(process.isAlive()) + .map { process => + new UnsealedProcess[F] { + def isAlive = F.delay(process.isAlive()) def exitValue = isAlive.ifM( evalOnVirtualThreadIfAvailable(F.interruptible(process.waitFor())), F.delay(process.exitValue()) ) - def stdin = writeOutputStreamCancelable( - F.delay(process.getOutputStream()), - F.blocking(process.destroy()) - ) + def stdin = writeOutputStreamCancelable( + F.delay(process.getOutputStream()), + F.blocking(process.destroy()) + ) - def stdout = readInputStreamCancelable( - F.delay(process.getInputStream()), - F.blocking(process.destroy()), - 8192 - ) + def stdout = readInputStreamCancelable( + F.delay(process.getInputStream()), + F.blocking(process.destroy()), + 8192 + ) - def stderr = readInputStreamCancelable( - F.delay(process.getErrorStream()), - F.blocking(process.destroy()), - 8192 - ) - } + def stderr = readInputStreamCancelable( + F.delay(process.getErrorStream()), + F.blocking(process.destroy()), + 8192 + ) + } } } } diff --git a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala index 9546efbacb..041982ac64 100644 --- a/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala +++ b/io/shared/src/main/scala/fs2/io/process/ProcessBuilder.scala @@ -74,14 +74,14 @@ sealed abstract class ProcessBuilder private { def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder /* @param mode The mode for handling stdin - */ - def redirectInput(mode: StreamRedirect): ProcessBuilder = + */ + def redirectInput(mode: StreamRedirect): ProcessBuilder = withOutputConfig(outputConfig.copy(stdin = mode)) - def redirectOutput(mode: StreamRedirect): ProcessBuilder = + def redirectOutput(mode: StreamRedirect): ProcessBuilder = withOutputConfig(outputConfig.copy(stdout = mode)) - def redirectError(mode: StreamRedirect): ProcessBuilder = + def redirectError(mode: StreamRedirect): ProcessBuilder = withOutputConfig(outputConfig.copy(stderr = mode)) /** Starts the process and returns a handle for interacting with it. @@ -93,10 +93,10 @@ sealed abstract class ProcessBuilder private { sealed abstract class StreamRedirect object StreamRedirect { - case object Pipe extends StreamRedirect - case object Inherit extends StreamRedirect - case object Discard extends StreamRedirect - final case class File(path: Path) extends StreamRedirect + case object Pipe extends StreamRedirect + case object Inherit extends StreamRedirect + case object Discard extends StreamRedirect + final case class File(path: Path) extends StreamRedirect } final case class ProcessOutputConfig( @@ -135,6 +135,7 @@ object ProcessBuilder { def withCurrentWorkingDirectory: ProcessBuilder = copy(workingDirectory = None) - def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder = copy(outputConfig = outputConfig) + def withOutputConfig(outputConfig: ProcessOutputConfig): ProcessBuilder = + copy(outputConfig = outputConfig) } } diff --git a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala index 945aea9e9f..4274cb2e8b 100644 --- a/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala +++ b/io/shared/src/test/scala/fs2/io/process/ProcessSuite.scala @@ -48,7 +48,11 @@ class ProcessSuite extends Fs2IoSuite { } test("stdout and stderr") { - ProcessBuilder("node", "-e", "console.log('good day stdout'); console.error('how do you do stderr')") + ProcessBuilder( + "node", + "-e", + "console.log('good day stdout'); console.error('how do you do stderr')" + ) .withOutputConfig(ProcessOutputConfig()) .spawn[IO] .use { p => @@ -72,7 +76,9 @@ class ProcessSuite extends Fs2IoSuite { test("merged stdout and stderr") { ProcessBuilder("node", "-e", "console.log('merged stdout'); console.error('merged stderr')") - .withOutputConfig(ProcessOutputConfig(stdout = StreamRedirect.Pipe, stderr = StreamRedirect.Pipe)) + .withOutputConfig( + ProcessOutputConfig(stdout = StreamRedirect.Pipe, stderr = StreamRedirect.Pipe) + ) .spawn[IO] .use { p => p.stdout @@ -89,8 +95,8 @@ class ProcessSuite extends Fs2IoSuite { .withOutputConfig(ProcessOutputConfig(stdout = StreamRedirect.File(path))) .spawn[IO] .use(_.exitValue) - .assertEquals(0) *> - Files[IO].readUtf8(path).compile.string.assertEquals("file output test\n") + .assertEquals(0) *> + Files[IO].readUtf8(path).compile.string.assertEquals("file output test\n") } } @@ -107,8 +113,14 @@ class ProcessSuite extends Fs2IoSuite { .withOutputConfig(ProcessOutputConfig(stdin = StreamRedirect.Pipe)) .spawn[IO] .use { p => - val input = Stream.emit("piped input test").through(fs2.text.utf8.encode).through(p.stdin).compile.drain - val output = p.stdout.through(fs2.text.utf8.decode).compile.string.assertEquals("piped input test") + val input = Stream + .emit("piped input test") + .through(fs2.text.utf8.encode) + .through(p.stdin) + .compile + .drain + val output = + p.stdout.through(fs2.text.utf8.decode).compile.string.assertEquals("piped input test") input *> output } } From 817865bfda209d37cc7d4f2d671a5a6cab5aaeab Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Fri, 11 Apr 2025 08:34:00 +0530 Subject: [PATCH 10/18] Passing Tests --- .../fs2/io/process/ProcessesPlatform.scala | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index c106b0e87e..cc44714fb8 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -36,16 +36,13 @@ private[process] trait ProcessesCompanionPlatform { def spawn(process: ProcessBuilder): Resource[F, Process[F]] = Resource { F.async_[(Process[F], F[Unit])] { cb => - - val spawnOptions = new facade.child_process.SpawnOptions { - "cwd" -> process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) - "env" ->( - if (process.inheritEnv) - (facade.process.env ++ process.extraEnv).toJSDictionary - else - process.extraEnv.toJSDictionary) - } + "cwd" -> process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) + "env" -> (if (process.inheritEnv) + (facade.process.env ++ process.extraEnv).toJSDictionary + else + process.extraEnv.toJSDictionary) + } val childProcess = facade.child_process.spawn( process.command, @@ -55,26 +52,26 @@ private[process] trait ProcessesCompanionPlatform { process.outputConfig.stdin match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] case StreamRedirect.File(path) => spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") - case StreamRedirect.Pipe => //Default behaviour + case StreamRedirect.Pipe => // Default behaviour } process.outputConfig.stdout match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] case StreamRedirect.File(path) => spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") - case StreamRedirect.Pipe => //Default behaviour + case StreamRedirect.Pipe => // Default behaviour } process.outputConfig.stderr match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] + case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] case StreamRedirect.File(path) => spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") - case StreamRedirect.Pipe => //Default behaviour + case StreamRedirect.Pipe => // Default behaviour } val fs2Process = new UnsealedProcess[F] { @@ -112,7 +109,7 @@ private[process] trait ProcessesCompanionPlatform { } else { childProcess.kill() childProcess.once("exit", () => cb(Either.unit)) - Left(None) + Left(None) } } } From 904173a7d8e43004479f6ace4a2dce8eb4548260 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Fri, 11 Apr 2025 08:59:11 +0530 Subject: [PATCH 11/18] Fixed ioJS --- .../fs2/io/process/ProcessesPlatform.scala | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index cc44714fb8..2704efd1d6 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -37,43 +37,46 @@ private[process] trait ProcessesCompanionPlatform { Resource { F.async_[(Process[F], F[Unit])] { cb => val spawnOptions = new facade.child_process.SpawnOptions { - "cwd" -> process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) - "env" -> (if (process.inheritEnv) + cwd = process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) + env = (if (process.inheritEnv) (facade.process.env ++ process.extraEnv).toJSDictionary else process.extraEnv.toJSDictionary) } - val childProcess = facade.child_process.spawn( - process.command, - process.args.toJSArray, - spawnOptions.asInstanceOf[facade.child_process.SpawnOptions] - ) - - process.outputConfig.stdin match { + + val stdinOpt = process.outputConfig.stdin match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") case StreamRedirect.Pipe => // Default behaviour } - - process.outputConfig.stdout match { + + val stdoutOpt = process.outputConfig.stdout match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") case StreamRedirect.Pipe => // Default behaviour } - - process.outputConfig.stderr match { + + val stderrOpt = process.outputConfig.stderr match { case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.File(path) => + spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") case StreamRedirect.Pipe => // Default behaviour } + spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt).asInstanceOf[js.Any] + + val childProcess = facade.child_process.spawn( + process.command, + process.args.toJSArray, + spawnOptions + ) + val fs2Process = new UnsealedProcess[F] { def isAlive: F[Boolean] = F.delay { From 068fed165be9ad7c55f56387d1823e74ebe9f8ff Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Fri, 11 Apr 2025 09:20:34 +0530 Subject: [PATCH 12/18] Fixed ioJS --- .../fs2/io/process/ProcessesPlatform.scala | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 2704efd1d6..9f1d9493db 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -46,26 +46,23 @@ private[process] trait ProcessesCompanionPlatform { val stdinOpt = process.outputConfig.stdin match { - case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" + case StreamRedirect.File(path) => path.toString case StreamRedirect.Pipe => // Default behaviour } val stdoutOpt = process.outputConfig.stdout match { - case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" + case StreamRedirect.File(path) => path.toString case StreamRedirect.Pipe => // Default behaviour } val stderrOpt = process.outputConfig.stderr match { - case StreamRedirect.Inherit => spawnOptions.stdio = "inherit".asInstanceOf[js.Any] - case StreamRedirect.Discard => spawnOptions.stdio = "ignore".asInstanceOf[js.Any] - case StreamRedirect.File(path) => - spawnOptions.stdio = js.Array(path.toString, "pipe", "pipe") + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" + case StreamRedirect.File(path) => path.toString case StreamRedirect.Pipe => // Default behaviour } From e9376b20fe10619f605ec805b1bb6cfe889b5c11 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Fri, 11 Apr 2025 09:28:22 +0530 Subject: [PATCH 13/18] sbt Format --- .../fs2/io/process/ProcessesPlatform.scala | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 9f1d9493db..f5a45e734f 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -38,32 +38,32 @@ private[process] trait ProcessesCompanionPlatform { F.async_[(Process[F], F[Unit])] { cb => val spawnOptions = new facade.child_process.SpawnOptions { cwd = process.workingDirectory.fold[js.UndefOr[String]](js.undefined)(_.toString) - env = (if (process.inheritEnv) - (facade.process.env ++ process.extraEnv).toJSDictionary - else - process.extraEnv.toJSDictionary) + env = + if (process.inheritEnv) + (facade.process.env ++ process.extraEnv).toJSDictionary + else + process.extraEnv.toJSDictionary } - val stdinOpt = process.outputConfig.stdin match { - case StreamRedirect.Inherit => "inherit" - case StreamRedirect.Discard => "ignore" + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.Pipe => // Default behaviour } - + val stdoutOpt = process.outputConfig.stdout match { - case StreamRedirect.Inherit => "inherit" - case StreamRedirect.Discard => "ignore" + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.Pipe => // Default behaviour } - + val stderrOpt = process.outputConfig.stderr match { - case StreamRedirect.Inherit => "inherit" - case StreamRedirect.Discard => "ignore" + case StreamRedirect.Inherit => "inherit" + case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.Pipe => // Default behaviour } spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt).asInstanceOf[js.Any] @@ -73,7 +73,7 @@ private[process] trait ProcessesCompanionPlatform { process.args.toJSArray, spawnOptions ) - + val fs2Process = new UnsealedProcess[F] { def isAlive: F[Boolean] = F.delay { From d31b605c50549c096c3282e0176c278695754705 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sun, 13 Apr 2025 01:03:21 +0530 Subject: [PATCH 14/18] Added Merge function --- io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index f5a45e734f..a30ff94440 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -100,6 +100,10 @@ private[process] trait ProcessesCompanionPlatform { def stdout = unsafeReadReadable(childProcess.stdout) def stderr = unsafeReadReadable(childProcess.stderr) + + def mergedOutput: Stream[F, Byte] = + stdout.merge(stderr) + } val finalize = F.asyncCheckAttempt[Unit] { cb => From 9483bab62443118ba749198776b532c2b409545d Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sun, 13 Apr 2025 01:07:28 +0530 Subject: [PATCH 15/18] scalafmt --- io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index a30ff94440..c8bd305c3c 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -101,9 +101,9 @@ private[process] trait ProcessesCompanionPlatform { def stderr = unsafeReadReadable(childProcess.stderr) - def mergedOutput: Stream[F, Byte] = - stdout.merge(stderr) - + def mergedOutput: Stream[F, Byte] = + stdout.merge(stderr) + } val finalize = F.asyncCheckAttempt[Unit] { cb => From cfa82aed40428999798132cf990eb5cf631af5ea Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sun, 13 Apr 2025 01:23:21 +0530 Subject: [PATCH 16/18] Passing tests --- .../fs2/io/process/ProcessesPlatform.scala | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index c8bd305c3c..a03c157958 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -45,28 +45,28 @@ private[process] trait ProcessesCompanionPlatform { process.extraEnv.toJSDictionary } - val stdinOpt = process.outputConfig.stdin match { + val stdinOpt: js.Any = process.outputConfig.stdin match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" - case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.File(path) => "pipe" + case StreamRedirect.Pipe => "pipe" } - val stdoutOpt = process.outputConfig.stdout match { + val stdoutOpt: js.Any = process.outputConfig.stdout match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" - case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.File(path) => "pipe" + case StreamRedirect.Pipe => "pipe" } - val stderrOpt = process.outputConfig.stderr match { + val stderrOpt: js.Any = process.outputConfig.stderr match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" - case StreamRedirect.File(path) => path.toString - case StreamRedirect.Pipe => // Default behaviour + case StreamRedirect.File(path) => "pipe" + case StreamRedirect.Pipe => "pipe" } - spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt).asInstanceOf[js.Any] + spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt) val childProcess = facade.child_process.spawn( process.command, @@ -97,9 +97,17 @@ private[process] trait ProcessesCompanionPlatform { def stdin = writeWritable(F.delay(childProcess.stdin)) - def stdout = unsafeReadReadable(childProcess.stdout) + def stdout = + if (process.outputConfig.stdout == StreamRedirect.Pipe) + unsafeReadReadable(childProcess.stdout) + else + Stream.empty - def stderr = unsafeReadReadable(childProcess.stderr) + def stderr = + if (process.outputConfig.stderr == StreamRedirect.Pipe) + unsafeReadReadable(childProcess.stderr) + else + Stream.empty def mergedOutput: Stream[F, Byte] = stdout.merge(stderr) From c13ae8dc7d160d988e94da48d2c8a3feb881efe9 Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sun, 13 Apr 2025 01:34:53 +0530 Subject: [PATCH 17/18] Passing tests --- io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index a03c157958..7566a86230 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -49,21 +49,21 @@ private[process] trait ProcessesCompanionPlatform { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => "pipe" + case StreamRedirect.Pipe => } val stdoutOpt: js.Any = process.outputConfig.stdout match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => "pipe" + case StreamRedirect.Pipe => } val stderrOpt: js.Any = process.outputConfig.stderr match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => "pipe" + case StreamRedirect.Pipe => } spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt) From d9be98ec70a393e61131ae74ad76bd363202544a Mon Sep 17 00:00:00 2001 From: sauren-sharma Date: Sun, 13 Apr 2025 01:39:17 +0530 Subject: [PATCH 18/18] scalafmt --- io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala index 7566a86230..4c27a0b840 100644 --- a/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala +++ b/io/js/src/main/scala/fs2/io/process/ProcessesPlatform.scala @@ -49,21 +49,21 @@ private[process] trait ProcessesCompanionPlatform { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => + case StreamRedirect.Pipe => } val stdoutOpt: js.Any = process.outputConfig.stdout match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => + case StreamRedirect.Pipe => } val stderrOpt: js.Any = process.outputConfig.stderr match { case StreamRedirect.Inherit => "inherit" case StreamRedirect.Discard => "ignore" case StreamRedirect.File(path) => "pipe" - case StreamRedirect.Pipe => + case StreamRedirect.Pipe => } spawnOptions.stdio = js.Array(stdinOpt, stdoutOpt, stderrOpt)