[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.
This commit is contained in:
eugene yokota 2026-02-02 11:03:50 -05:00 committed by GitHub
parent 6e974bb283
commit 2380082bcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 9 deletions

View File

@ -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.*"),
),
)

View File

@ -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()
}
}

View File

@ -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()