diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index a0d78d70e..0e2dae927 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -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 + } } diff --git a/server-test/src/server-test/client/build.sbt b/server-test/src/server-test/client/build.sbt index 686d2a7a8..965179d66 100644 --- a/server-test/src/server-test/client/build.sbt +++ b/server-test/src/server-test/client/build.sbt @@ -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) diff --git a/server-test/src/server-test/client/src/main/scala/A.scala b/server-test/src/server-test/client/src/main/scala/A.scala index 171b96e91..008ed02cd 100644 --- a/server-test/src/server-test/client/src/main/scala/A.scala +++ b/server-test/src/server-test/client/src/main/scala/A.scala @@ -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!") diff --git a/server-test/src/test/scala/testpkg/ClientTest.scala b/server-test/src/test/scala/testpkg/ClientTest.scala index 239fddc6b..cb4d6a7d4 100644 --- a/server-test/src/test/scala/testpkg/ClientTest.scala +++ b/server-test/src/test/scala/testpkg/ClientTest.scala @@ -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")}" + ) + } }