diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 7ba2681fa..8b9cf7013 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -424,6 +424,15 @@ private[sbt] final class CommandExchange { tryTo(_.notifyEvent(event))(c) case _ => + // Route a log event to a specific channel, independent of currentExec. + // Used for background job output so messages reach the originating client + // even after the spawning task has completed and currentExec has been cleared. + private[sbt] def logMessage(channelName: String, event: LogEvent): Unit = + channels.foreach: + case c: NetworkChannel if c.subscribeToAll || c.name == channelName => + tryTo(_.notifyEvent(event))(c) + case _ => + private def isChannelOwner(c: NetworkChannel): Boolean = currentExec.exists(_.source.exists(_.channelName == c.name)) diff --git a/main/src/main/scala/sbt/internal/LogManager.scala b/main/src/main/scala/sbt/internal/LogManager.scala index 351de1984..596bd9e4c 100644 --- a/main/src/main/scala/sbt/internal/LogManager.scala +++ b/main/src/main/scala/sbt/internal/LogManager.scala @@ -123,7 +123,14 @@ object LogManager { context: LoggerContext ): ManagedLogger = { val console = ConsoleAppender.safe("bg-" + ConsoleAppender.generateName(), ITerminal.current) - LogManager.backgroundLog(data, state, task, console, relay(()), context) + // Use a channel-aware relay appender so background job log output reaches + // the originating client even after the spawning task completes and + // currentExec is cleared. + val channelName = state.currentCommand.flatMap(_.source.map(_.channelName)) + val bgRelay = channelName match + case Some(_) => new RelayAppender("bg-Relay" + generateId.incrementAndGet, channelName) + case None => relay(()) + LogManager.backgroundLog(data, state, task, console, bgRelay, context) } } diff --git a/main/src/main/scala/sbt/internal/RelayAppender.scala b/main/src/main/scala/sbt/internal/RelayAppender.scala index 8af712fef..035ced89b 100644 --- a/main/src/main/scala/sbt/internal/RelayAppender.scala +++ b/main/src/main/scala/sbt/internal/RelayAppender.scala @@ -13,14 +13,19 @@ import sbt.internal.util.* import sbt.protocol.LogEvent import sbt.util.Level -class RelayAppender(override val name: String) +class RelayAppender(override val name: String, targetChannel: Option[String] = None) extends ConsoleAppender( name, ConsoleAppender.Properties.from(ConsoleOut.NullConsoleOut, true, true), _ => None ) { + def this(name: String) = this(name, None) + lazy val exchange = StandardMain.exchange override def appendLog(level: Level.Value, message: => String): Unit = { - exchange.logMessage(LogEvent(level = level.toString, message = message)) + val event = LogEvent(level = level.toString, message = message) + targetChannel match + case Some(ch) => exchange.logMessage(ch, event) + case None => exchange.logMessage(event) } } diff --git a/main/src/main/scala/sbt/internal/RunUtil.scala b/main/src/main/scala/sbt/internal/RunUtil.scala index afbd0a5a1..1fb0376f8 100644 --- a/main/src/main/scala/sbt/internal/RunUtil.scala +++ b/main/src/main/scala/sbt/internal/RunUtil.scala @@ -293,9 +293,16 @@ object RunUtil: val (mainClass, allArgs) = parser.parsed val (jvmArgs, appArgs) = splitArgs(allArgs) val hashClasspath = (bgRunMain / bgHashClasspath).value - val fo = (run / forkOptions).value + // Background runs must not inherit the terminal's stdin/stdout via connectInput. + // Without this, ForkRun.configLogged skips setting LoggedOutput, causing + // the fork to use inheritIO() which bypasses the background logger entirely. + val fo = (run / forkOptions).value.withConnectInput(false) val log = streams.value.log - val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log) + // applyJvmArgs only builds a new ForkRun when jvmArgs is non-empty, so force + // construction of a ForkRun that uses the updated ForkOptions above. + val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log) match + case (_: ForkRun, _) => (new ForkRun(fo.withRunJVMOptions(fo.runJVMOptions ++ jvmArgs)), fo) + case other => other val wrapper = termWrapper(canonicalInput.value, echoInput.value) val converter = fileConverter.value setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value)) @@ -337,9 +344,16 @@ object RunUtil: val service = bgJobService.value val mainClass = getMainClass(mainClassTask.value) val hashClasspath = (bgRun / bgHashClasspath).value - val fo = (run / forkOptions).value + // Background runs must not inherit the terminal's stdin/stdout via connectInput. + // Without this, ForkRun.configLogged skips setting LoggedOutput, causing + // the fork to use inheritIO() which bypasses the background logger entirely. + val fo = (run / forkOptions).value.withConnectInput(false) val log = streams.value.log - val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log) + // applyJvmArgs only builds a new ForkRun when jvmArgs is non-empty, so force + // construction of a ForkRun that uses the updated ForkOptions above. + val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log) match + case (_: ForkRun, _) => (new ForkRun(fo.withRunJVMOptions(fo.runJVMOptions ++ jvmArgs)), fo) + case other => other val wrapper = termWrapper(canonicalInput.value, echoInput.value) val converter = fileConverter.value setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value)) diff --git a/sbt-app/src/sbt-test/run/bg/build.sbt b/sbt-app/src/sbt-test/run/bg/build.sbt new file mode 100644 index 000000000..da0e33161 --- /dev/null +++ b/sbt-app/src/sbt-test/run/bg/build.sbt @@ -0,0 +1,43 @@ +import sbt.internal.LogManager +import sbt.internal.util.{ Appender, ConsoleAppender, ConsoleOut } +import java.io.{ FileWriter, PrintWriter } + +lazy val checkBgOutput = taskKey[Unit]("Verify the bgRun forked process output was logged") +lazy val waitForAllBgJobs = taskKey[Unit]("Wait for every bgJobService job to terminate") +lazy val outFile = settingKey[File]("File where bgRun output is captured for the test") + +lazy val root = project + .in(file(".")) + .settings( + run / fork := true, + outFile := baseDirectory.value / "target" / "bg-output.log", + // Override logManager so the background logger's relay appender writes + // to a file. This lets the test assert that forked-process output + // reached the managed logger (rather than going to the JVM's stdout via + // inheritIO, which is what happens when the bgRun fork-output bug is + // present). In a scripted test there are no network channels, so the + // default relay appender has no observable effect anyway. + logManager := { + val ea = extraAppenders.value + val f = outFile.value + IO.touch(f) + val fileRelay: Unit => Appender = _ => { + val pw = new PrintWriter(new FileWriter(f, true), true) + ConsoleAppender("bg-file-test", ConsoleOut.printWriterOut(pw)) + } + LogManager.withLoggers( + screen = (task, state) => ConsoleAppender(s"screen-${task.key.label}"), + relay = fileRelay, + extra = ea + ) + }, + waitForAllBgJobs := Def.uncached { + val service = bgJobService.value + service.jobs.foreach(service.waitFor) + }, + checkBgOutput := Def.uncached { + val f = outFile.value + val content = IO.read(f) + assert(content.contains("foobar"), s"Expected 'foobar' in $f, got:\n$content") + } + ) diff --git a/sbt-app/src/sbt-test/run/bg/src/main/scala/Test.scala b/sbt-app/src/sbt-test/run/bg/src/main/scala/Test.scala new file mode 100644 index 000000000..8adfe186e --- /dev/null +++ b/sbt-app/src/sbt-test/run/bg/src/main/scala/Test.scala @@ -0,0 +1 @@ +@main def test: Unit = println("foobar") diff --git a/sbt-app/src/sbt-test/run/bg/test b/sbt-app/src/sbt-test/run/bg/test new file mode 100644 index 000000000..76408a703 --- /dev/null +++ b/sbt-app/src/sbt-test/run/bg/test @@ -0,0 +1,3 @@ +> bgRun +> waitForAllBgJobs +> checkBgOutput