mirror of https://github.com/sbt/sbt.git
[1.x] fix: Pump forked stdout/stderr to the captured active terminal (#9185)
Since #8678 (sbt 1.12.2), Fork.apply has routed the `connectInput && outputStrategy == StdoutOutput` case through a new JProcess path that calls `jpb.inheritIO()`. `inheritIO` ties the child JVM's stdio to the sbt server JVM's OS-level fd 1/2, bypassing the `Terminal` indirection. In server mode (which `sbt --client` uses), that indirection is how task output reaches the client terminal — so the child's stdout/stderr went nowhere observable to the client. Keep `redirectInput(Redirect.INHERIT)` so REPL/raw-mode keystrokes still reach the child (preserving the use case #8678 added), but leave stdout/stderr as PIPE and pump them to the active terminal's output/error streams in two daemon threads. The pump threads are joined after `p.waitFor()` so all buffered output drains before the task completes. The active terminal is captured once on the task thread rather than re-resolved per-write via the `System.out`/`err` proxies, otherwise a concurrent terminal swap can route mid-pump bytes to the wrong client (observed on the CI JDK 8 matrix; the proxy's `activeTerminal.get()` is evaluated at every write). Forced scala/scala3 CI to downgrade from sbt 1.12.10 to 1.12.1 (see scala/scala3#25995). Diagnosis credit: @mbovel in #9185. Fixes sbt/sbt#9185. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2fe2e10cc9
commit
0c03718218
|
|
@ -8,12 +8,11 @@
|
|||
|
||||
package sbt
|
||||
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.{ File, IOException, InputStream, OutputStream, PrintWriter }
|
||||
import java.lang.ProcessBuilder.Redirect
|
||||
import scala.sys.process.Process
|
||||
import OutputStrategy._
|
||||
import sbt.internal.util.{ RunningProcesses, Util }
|
||||
import sbt.internal.util.{ RunningProcesses, Terminal, Util }
|
||||
import Util.{ AnyOps, none }
|
||||
|
||||
import java.lang.{ ProcessBuilder => JProcessBuilder, Process => JProcess }
|
||||
|
|
@ -214,14 +213,24 @@ object Fork {
|
|||
val environment: List[(String, String)] = env.toList ++ extraEnv
|
||||
workingDirectory.foreach(jpb.directory(_))
|
||||
environment.foreach { case (k, v) => jpb.environment.put(k, v) }
|
||||
jpb.inheritIO()
|
||||
// Inherit stdin for REPL/raw-mode keystrokes. Stdout/stderr stay PIPE so
|
||||
// `blockJForExitCode` can pump them to the active terminal directly.
|
||||
jpb.redirectInput(Redirect.INHERIT)
|
||||
jpb.start()
|
||||
}
|
||||
|
||||
private[sbt] def blockJForExitCode(p: JProcess): Int = {
|
||||
RunningProcesses.add(p)
|
||||
try {
|
||||
// Capture the active terminal's streams once on the task thread. The
|
||||
// System.out/err proxy resolves `activeTerminal` at every write, so a
|
||||
// concurrent terminal swap can route mid-pump bytes to the wrong client.
|
||||
val active = Terminal.current
|
||||
val outT = pumpStream(p.getInputStream, active.outputStream, "sbt-fork-stdout")
|
||||
val errT = pumpStream(p.getErrorStream, active.errorStream, "sbt-fork-stderr")
|
||||
p.waitFor()
|
||||
outT.join()
|
||||
errT.join()
|
||||
p.exitValue()
|
||||
} finally {
|
||||
if (p.isAlive()) p.destroy()
|
||||
|
|
@ -237,4 +246,23 @@ object Fork {
|
|||
RunningProcesses.remove(p)
|
||||
}
|
||||
}
|
||||
|
||||
private[sbt] def pumpStream(in: InputStream, out: OutputStream, name: String): Thread = {
|
||||
val t = new Thread(name) {
|
||||
setDaemon(true)
|
||||
override def run(): Unit = {
|
||||
val buf = new Array[Byte](4096)
|
||||
try {
|
||||
var n = in.read(buf)
|
||||
while (n != -1) {
|
||||
out.write(buf, 0, n)
|
||||
out.flush()
|
||||
n = in.read(buf)
|
||||
}
|
||||
} catch { case _: IOException => () }
|
||||
}
|
||||
}
|
||||
t.start()
|
||||
t
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,3 +7,8 @@ TaskKey[Unit]("willFail") := { throw new Exception("failed") }
|
|||
libraryDependencies += "org.scalameta" %% "munit" % "1.0.4" % Test
|
||||
|
||||
TaskKey[Unit]("fooBar") := { () }
|
||||
|
||||
// Exercise the forked interactive code path (connectInput + StdoutOutput).
|
||||
run / fork := true
|
||||
run / connectInput := true
|
||||
run / outputStrategy := Some(StdoutOutput)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
class A
|
||||
|
||||
@main def hello() = println("Hello, World!")
|
||||
@main def hello() =
|
||||
println("STDOUT_MARKER_9185")
|
||||
Console.err.println("STDERR_MARKER_9185")
|
||||
println("Hello, World!")
|
||||
|
|
|
|||
|
|
@ -157,4 +157,12 @@ object ClientTest extends AbstractServerTest {
|
|||
test("quote with semi") { _ =>
|
||||
assert(complete("\"compile; fooB") == Vector("compile; fooBar"))
|
||||
}
|
||||
test("forked run with connectInput relays stdout to --client") { _ =>
|
||||
val (exit, lines) = clientWithStdoutLines("run")
|
||||
assert(exit == 0, s"non-zero exit; lines=${lines.mkString("\n")}")
|
||||
assert(
|
||||
lines.exists(_.contains("STDOUT_MARKER_9185")),
|
||||
s"missing STDOUT_MARKER_9185 in: ${lines.mkString("\n")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue