From 2380082bccf11be03b29fdebef2338fd7c88b905 Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Mon, 2 Feb 2026 11:03:50 -0500 Subject: [PATCH] [1.x] Use JProcess for interactive forking (#8678) **Problem/Solution** For forking that require both input and output, use a Java process instead of the Scala process. --- build.sbt | 3 +- .../sbt/internal/util/RunningProcesses.scala | 13 +++-- run/src/main/scala/sbt/Fork.scala | 50 +++++++++++++++++-- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/build.sbt b/build.sbt index 695456f8f..92f9349a1 100644 --- a/build.sbt +++ b/build.sbt @@ -180,7 +180,8 @@ def mimaSettingsSince(versions: Seq[String]): Seq[Def.Setting[_]] = Def settings exclude[DirectMissingMethodProblem]("sbt.PluginData.this"), exclude[IncompatibleResultTypeProblem]("sbt.EvaluateTask.executeProgress"), exclude[DirectMissingMethodProblem]("sbt.Keys.currentTaskProgress"), - exclude[IncompatibleResultTypeProblem]("sbt.PluginData.copy$default$10") + exclude[IncompatibleResultTypeProblem]("sbt.PluginData.copy$default$10"), + exclude[IncompatibleMethTypeProblem]("sbt.internal.util.*"), ), ) 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 e291da42e..74951ab4e 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 => JProcess } import java.util.concurrent.ConcurrentHashMap import scala.sys.process.Process @@ -18,17 +19,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 cb4f664d0..c83ec7a7f 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.{ AnyOps, none } -import java.lang.{ ProcessBuilder => JProcessBuilder } +import java.lang.{ ProcessBuilder => JProcessBuilder, Process => JProcess } import java.util.Locale /** @@ -33,8 +33,18 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * It is configured according to `config`. * If `runnerClass` is defined for this Fork instance, it is prepended to `arguments` to define the arguments passed to the forked command. */ - def apply(config: ForkOptions, arguments: Seq[String]): Int = - Fork.blockForExitCode(fork(config, arguments)) + def apply(config: ForkOptions, arguments: Seq[String]): Int = { + import config._ + val outStrategy = outputStrategy.getOrElse(StdoutOutput) + if (connectInput && outStrategy == StdoutOutput) + 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 to terminate the forked process. @@ -43,6 +53,14 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * If `runnerClass` is defined for this Fork instance, it is prepended to `arguments` to define the arguments passed to the forked command. */ 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) @@ -58,7 +76,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[this] def makeOptions( @@ -187,6 +205,30 @@ object Fork { } } + private[sbt] def forkInternalInteractive( + config: ForkOptions, + extraEnv: List[(String, String)], + jpb: JProcessBuilder + ): JProcess = { + import config.{ envVars => 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()