From 5131b8658a7e2601175aa7533901573392018438 Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Mon, 2 Feb 2026 11:03:28 -0500 Subject: [PATCH] [2.x] Use JProcess for interactive forking (#8677) **Problem/Solution** For forking that require both input and output, use a Java process instead of the Scala process. --- .../sbt/internal/util/RunningProcesses.scala | 13 +++-- run/src/main/scala/sbt/Fork.scala | 47 +++++++++++++++++-- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala index 67c5711f9..63ca7de4d 100644 --- a/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala +++ b/internal/util-control/src/main/scala/sbt/internal/util/RunningProcesses.scala @@ -8,6 +8,7 @@ package sbt.internal.util +import java.lang.{ Process as JProcess } import java.util.concurrent.ConcurrentHashMap import scala.sys.process.Process @@ -17,17 +18,21 @@ import scala.sys.process.Process * processes when the user inputs ctrl+c. */ private[sbt] object RunningProcesses { - val active = ConcurrentHashMap.newKeySet[Process] - def add(process: Process): Unit = active.synchronized { + val active = ConcurrentHashMap.newKeySet[AnyRef] + def add(process: AnyRef): Unit = active.synchronized { active.add(process) () } - def remove(process: Process): Unit = active.synchronized { + def remove(process: AnyRef): Unit = active.synchronized { active.remove(process) () } def killAll(): Unit = active.synchronized { - active.forEach(_.destroy()) + active.forEach { + case p: Process => p.destroy() + case p: JProcess => p.destroy() + case _ => () + } active.clear() } } diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index d738dfbd8..2c4c102af 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -16,7 +16,7 @@ import OutputStrategy.* import sbt.internal.util.{ RunningProcesses, Util } import Util.* -import java.lang.{ ProcessBuilder as JProcessBuilder } +import java.lang.{ ProcessBuilder as JProcessBuilder, Process as JProcess } import java.util.Locale /** @@ -36,7 +36,15 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * define the arguments passed to the forked command. */ def apply(config: ForkOptions, arguments: Seq[String]): Int = - Fork.blockForExitCode(fork(config, arguments)) + import config.* + val outStrategy = outputStrategy.getOrElse(StdoutOutput) + if connectInput && outStrategy == StdoutOutput then + Fork.blockJForExitCode(interactiveFork(config, arguments)) + else Fork.blockForExitCode(fork(config, arguments)) + + private def interactiveFork(config: ForkOptions, arguments: Seq[String]): JProcess = + val (extraEnv, jpb) = prep(config, arguments) + Fork.forkInternalInteractive(config, extraEnv, jpb) /** * Forks the configured process and returns a `Process` that can be used to wait for completion or @@ -45,7 +53,14 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * Fork instance, it is prepended to `arguments` to define the arguments passed to the forked * command. */ - def fork(config: ForkOptions, arguments: Seq[String]): Process = { + def fork(config: ForkOptions, arguments: Seq[String]): Process = + val (extraEnv, jpb) = prep(config, arguments) + Fork.forkInternal(config, extraEnv, jpb) + + private def prep( + config: ForkOptions, + arguments: Seq[String] + ): (List[(String, String)], JProcessBuilder) = import config.* val executable = Fork.javaCommand(javaHome, commandName).getAbsolutePath val preOptions = makeOptions(runJVMOptions, bootJars, arguments) @@ -63,8 +78,7 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { val extraEnv = classpathEnv.toList.map { value => Fork.ClasspathEnvKey -> value } - Fork.forkInternal(config, extraEnv, jpb) - } + (extraEnv, jpb) private def makeOptions( jvmOptions: Seq[String], @@ -196,6 +210,29 @@ object Fork { case out: CustomInputOutput => process.run(out.processIO) } + private[sbt] def forkInternalInteractive( + config: ForkOptions, + extraEnv: List[(String, String)], + jpb: JProcessBuilder + ): JProcess = + import config.{ envVars as env, * } + val environment: List[(String, String)] = env.toList ++ extraEnv + workingDirectory.foreach(jpb.directory(_)) + environment.foreach { case (k, v) => jpb.environment.put(k, v) } + jpb.inheritIO() + jpb.start() + + private[sbt] def blockJForExitCode(p: JProcess): Int = { + RunningProcesses.add(p) + try + p.waitFor() + p.exitValue() + finally { + if (p.isAlive()) p.destroy() + RunningProcesses.remove(p) + } + } + private[sbt] def blockForExitCode(p: Process): Int = { RunningProcesses.add(p) try p.exitValue()