mirror of https://github.com/sbt/sbt.git
[2.x] Fix console output not appearing with bgRun and run / fork := true (#9166)
Bug 1: bgRun forks with inheritIO() instead of LoggedOutput bgRunTask / bgRunMainTask resolve their fork options via (run / forkOptions), which inherits run / connectInput := true. That has two downstream consequences: 1. ForkRun.configLogged (in run/src/main/scala/sbt/Run.scala) skips installing OutputStrategy.LoggedOutput(log) whenever config.connectInput == true. 2. Fork.apply (in run/src/main/scala/sbt/Fork.scala) sees connectInput && outputStrategy == StdoutOutput and takes the interactiveFork path, which calls jpb.inheritIO(). Bug 2: Background-job log relay drops messages after the spawning task ends Even after Bug 1 is fixed, LoggedOutput routes the forked process's stdout into the background ManagedLogger, whose appenders include a shared RelayAppender. The relay calls CommandExchange.logMessage(event), which uses isChannelOwner(c) to pick the target channel.
This commit is contained in:
parent
0af7c2f4a6
commit
5757955fc7
|
|
@ -424,6 +424,15 @@ private[sbt] final class CommandExchange {
|
||||||
tryTo(_.notifyEvent(event))(c)
|
tryTo(_.notifyEvent(event))(c)
|
||||||
case _ =>
|
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 =
|
private def isChannelOwner(c: NetworkChannel): Boolean =
|
||||||
currentExec.exists(_.source.exists(_.channelName == c.name))
|
currentExec.exists(_.source.exists(_.channelName == c.name))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,14 @@ object LogManager {
|
||||||
context: LoggerContext
|
context: LoggerContext
|
||||||
): ManagedLogger = {
|
): ManagedLogger = {
|
||||||
val console = ConsoleAppender.safe("bg-" + ConsoleAppender.generateName(), ITerminal.current)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,19 @@ import sbt.internal.util.*
|
||||||
import sbt.protocol.LogEvent
|
import sbt.protocol.LogEvent
|
||||||
import sbt.util.Level
|
import sbt.util.Level
|
||||||
|
|
||||||
class RelayAppender(override val name: String)
|
class RelayAppender(override val name: String, targetChannel: Option[String] = None)
|
||||||
extends ConsoleAppender(
|
extends ConsoleAppender(
|
||||||
name,
|
name,
|
||||||
ConsoleAppender.Properties.from(ConsoleOut.NullConsoleOut, true, true),
|
ConsoleAppender.Properties.from(ConsoleOut.NullConsoleOut, true, true),
|
||||||
_ => None
|
_ => None
|
||||||
) {
|
) {
|
||||||
|
def this(name: String) = this(name, None)
|
||||||
|
|
||||||
lazy val exchange = StandardMain.exchange
|
lazy val exchange = StandardMain.exchange
|
||||||
override def appendLog(level: Level.Value, message: => String): Unit = {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,9 +293,16 @@ object RunUtil:
|
||||||
val (mainClass, allArgs) = parser.parsed
|
val (mainClass, allArgs) = parser.parsed
|
||||||
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
||||||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
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 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 wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
setWindowTitle(mkWindowTitle("bgRunMain", organization.value, name.value, version.value))
|
||||||
|
|
@ -337,9 +344,16 @@ object RunUtil:
|
||||||
val service = bgJobService.value
|
val service = bgJobService.value
|
||||||
val mainClass = getMainClass(mainClassTask.value)
|
val mainClass = getMainClass(mainClassTask.value)
|
||||||
val hashClasspath = (bgRun / bgHashClasspath).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 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 wrapper = termWrapper(canonicalInput.value, echoInput.value)
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
setWindowTitle(mkWindowTitle("bgRun", organization.value, name.value, version.value))
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@main def test: Unit = println("foobar")
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
> bgRun
|
||||||
|
> waitForAllBgJobs
|
||||||
|
> checkBgOutput
|
||||||
Loading…
Reference in New Issue