mirror of https://github.com/sbt/sbt.git
Merge pull request #9169 from eed3si9n/bport/i9100
[2.0.x] bport: Run related fixes
This commit is contained in:
commit
7088ad9884
|
|
@ -318,6 +318,7 @@ class NetworkClient(
|
|||
token = tkn,
|
||||
skipAnalysis = Some(skipAnalysis),
|
||||
canWork = Some(true),
|
||||
subscribeToAll = Some(false),
|
||||
)
|
||||
val initCommand = InitCommand(
|
||||
token = tkn, // duplicated with opts for compatibility
|
||||
|
|
|
|||
|
|
@ -1976,11 +1976,15 @@ object Defaults extends BuildCommon {
|
|||
val parser =
|
||||
loadForParser(discoveredMainClasses)((s, names) => runMainParser(s, names getOrElse Nil))
|
||||
Def.inputTask {
|
||||
val (mainClass, args) = parser.parsed
|
||||
val (mainClass, allArgs) = parser.parsed
|
||||
val (jvmArgs, appArgs) = RunUtil.splitArgs(allArgs)
|
||||
val cp = classpath.value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
given FileConverter = fileConverter.value
|
||||
scalaRun.value
|
||||
.run(mainClass, cp.files, args, streams.value.log)
|
||||
val (modifiedRun, _) = RunUtil.applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
modifiedRun
|
||||
.run(mainClass, cp.files, appArgs, log)
|
||||
.get
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -366,12 +366,11 @@ private[sbt] final class CommandExchange {
|
|||
}
|
||||
|
||||
// This is an interface to directly notify events.
|
||||
private[sbt] def notifyEvent[A: JsonFormat](method: String, params: A): Unit = {
|
||||
channels.foreach {
|
||||
case c: NetworkChannel => tryTo(_.notifyEvent(method, params))(c)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
private[sbt] def notifyEvent[A: JsonFormat](method: String, params: A): Unit =
|
||||
channels.foreach:
|
||||
case c: NetworkChannel if c.subscribeToAll || isChannelOwner(c) =>
|
||||
tryTo(_.notifyEvent(method, params))(c)
|
||||
case _ =>
|
||||
|
||||
private def tryTo(f: NetworkChannel => Unit)(
|
||||
channel: NetworkChannel
|
||||
|
|
@ -418,12 +417,23 @@ private[sbt] final class CommandExchange {
|
|||
}
|
||||
def unprompt(event: ConsoleUnpromptEvent): Unit = channels.foreach(_.unprompt(event))
|
||||
|
||||
def logMessage(event: LogEvent): Unit = {
|
||||
channels.foreach {
|
||||
case c: NetworkChannel => tryTo(_.notifyEvent(event))(c)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
def logMessage(event: LogEvent): Unit =
|
||||
channels.foreach:
|
||||
case c: NetworkChannel if c.subscribeToAll || isChannelOwner(c) =>
|
||||
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))
|
||||
|
||||
def notifyStatus(event: ExecStatusEvent): Unit = {
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,40 @@ import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo }
|
|||
import sbt.io.IO
|
||||
import sbt.protocol.Serialization
|
||||
import sbt.util.CacheImplicits.given
|
||||
import sbt.util.Logger
|
||||
import xsbti.FileConverter
|
||||
|
||||
object RunUtil:
|
||||
/**
|
||||
* Split arguments at the first `--` delimiter.
|
||||
* Tokens before `--` are JVM args; tokens after are app args.
|
||||
* If no `--` is present, all tokens are app args (backward compatible).
|
||||
*/
|
||||
private[sbt] def splitArgs(args: Seq[String]): (Seq[String], Seq[String]) =
|
||||
val idx = args.indexOf("--")
|
||||
if idx < 0 then (Nil, args)
|
||||
else (args.take(idx), args.drop(idx + 1))
|
||||
|
||||
/**
|
||||
* Apply CLI JVM args to the ScalaRun and ForkOptions.
|
||||
* For ForkRun: creates new ForkRun with augmented runJVMOptions.
|
||||
* For Run (non-fork): warns and returns unchanged.
|
||||
*/
|
||||
private[sbt] def applyJvmArgs(
|
||||
scalaRun: ScalaRun,
|
||||
jvmArgs: Seq[String],
|
||||
fo: ForkOptions,
|
||||
log: Logger,
|
||||
): (ScalaRun, ForkOptions) =
|
||||
if jvmArgs.isEmpty then (scalaRun, fo)
|
||||
else
|
||||
scalaRun match
|
||||
case _: ForkRun =>
|
||||
val newFo = fo.withRunJVMOptions(fo.runJVMOptions ++ jvmArgs)
|
||||
(new ForkRun(newFo), newFo)
|
||||
case _ =>
|
||||
(scalaRun, fo)
|
||||
|
||||
private def setWindowTitle(title: String): Unit =
|
||||
if System.console() != null && System.getenv("TERM") != null then
|
||||
scala.Console.print(s"\u001b]0;$title\u0007")
|
||||
|
|
@ -38,11 +69,14 @@ object RunUtil:
|
|||
): Initialize[InputTask[Unit]] =
|
||||
val parser = Def.spaceDelimited()
|
||||
Def.inputTask {
|
||||
val in = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val mainClass = getMainClass(mainClassTask.value)
|
||||
val cp = classpath.value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
given FileConverter = fileConverter.value
|
||||
scalaRun.value.run(mainClass, cp.files, in, streams.value.log).get
|
||||
val (modifiedRun, _) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
modifiedRun.run(mainClass, cp.files, appArgs, log).get
|
||||
}
|
||||
|
||||
def configTasks(c: ScopeAxis[ConfigKey]): Seq[Setting[?]] = Seq(
|
||||
|
|
@ -136,9 +170,12 @@ object RunUtil:
|
|||
val conv = fileConverter.value
|
||||
given FileConverter = conv
|
||||
val service = bgJobService.value
|
||||
val (mainClass, args) = parser.parsed
|
||||
val (mainClass, allArgs) = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
||||
val hashClasspath = (bgRunMain / bgHashClasspath).value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
val (modifiedRun, modifiedFo) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
val state = Keys.state.value
|
||||
val windowTitle = mkWindowTitle("runMain", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
|
|
@ -149,7 +186,8 @@ object RunUtil:
|
|||
workingDir,
|
||||
conv,
|
||||
)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
||||
val info =
|
||||
mkRunInfo(appArgs.toVector, mainClass, cp, modifiedFo, conv, Some(windowTitle))
|
||||
val result = ClientJobParams(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -168,15 +206,15 @@ object RunUtil:
|
|||
hashClasspath,
|
||||
conv,
|
||||
)
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
case sr =>
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||
service.waitForTry(handle).get
|
||||
()
|
||||
}
|
||||
|
|
@ -192,11 +230,13 @@ object RunUtil:
|
|||
Def.inputTask {
|
||||
val conv = fileConverter.value
|
||||
given FileConverter = conv
|
||||
val args = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val service = bgJobService.value
|
||||
val mainClass = getMainClass(mainClassTask.value)
|
||||
val hashClasspath = (bgRun / bgHashClasspath).value
|
||||
val fo = (run / forkOptions).value
|
||||
val log = streams.value.log
|
||||
val (modifiedRun, modifiedFo) = applyJvmArgs(scalaRun.value, jvmArgs, fo, log)
|
||||
val state = Keys.state.value
|
||||
val windowTitle = mkWindowTitle("run", organization.value, name.value, version.value)
|
||||
if clientRun.value && state.isNetworkCommand then
|
||||
|
|
@ -207,7 +247,7 @@ object RunUtil:
|
|||
workingDir,
|
||||
conv,
|
||||
)
|
||||
val info = mkRunInfo(args.toVector, mainClass, cp, fo, conv, Some(windowTitle))
|
||||
val info = mkRunInfo(appArgs.toVector, mainClass, cp, modifiedFo, conv, Some(windowTitle))
|
||||
val result = ClientJobParams(
|
||||
runInfo = info
|
||||
)
|
||||
|
|
@ -226,15 +266,15 @@ object RunUtil:
|
|||
hashClasspath,
|
||||
conv
|
||||
)
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
case sr =>
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||
service.waitForTry(handle).get
|
||||
()
|
||||
}
|
||||
|
|
@ -250,8 +290,19 @@ object RunUtil:
|
|||
)
|
||||
Def.inputTask {
|
||||
val service = bgJobService.value
|
||||
val (mainClass, args) = parser.parsed
|
||||
val (mainClass, allArgs) = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(allArgs)
|
||||
val hashClasspath = (bgRunMain / bgHashClasspath).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
|
||||
// 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))
|
||||
|
|
@ -268,15 +319,15 @@ object RunUtil:
|
|||
)
|
||||
else classpath.value
|
||||
given FileConverter = fileConverter.value
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
case sr =>
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,10 +340,20 @@ object RunUtil:
|
|||
): Initialize[InputTask[JobHandle]] =
|
||||
val parser = Def.spaceDelimited()
|
||||
Def.inputTask {
|
||||
val args = parser.parsed
|
||||
val (jvmArgs, appArgs) = splitArgs(parser.parsed)
|
||||
val service = bgJobService.value
|
||||
val mainClass = getMainClass(mainClassTask.value)
|
||||
val hashClasspath = (bgRun / bgHashClasspath).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
|
||||
// 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))
|
||||
|
|
@ -309,15 +370,15 @@ object RunUtil:
|
|||
)
|
||||
else classpath.value
|
||||
given FileConverter = converter
|
||||
scalaRun.value match
|
||||
modifiedRun match
|
||||
case r: Run =>
|
||||
val loader = r.newLoader(cp.files)
|
||||
(
|
||||
Some(loader),
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, args, logger).get)
|
||||
wrapper(() => r.runWithLoader(loader, cp.files, mainClass, appArgs, logger).get)
|
||||
)
|
||||
case sr =>
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, args, logger).get))
|
||||
(None, wrapper(() => sr.run(mainClass, cp.files, appArgs, logger).get))
|
||||
}
|
||||
}
|
||||
end RunUtil
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ private[sbt] object LanguageServerProtocol {
|
|||
)
|
||||
)
|
||||
val opt = Converter.fromJson[InitializeOption](optionJson).get
|
||||
setInitializeOption(opt)
|
||||
if (authOptions(ServerAuthentication.Token)) {
|
||||
val token = opt.token.getOrElse(sys.error("'token' is missing."))
|
||||
if (authenticate(token)) ()
|
||||
else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token")
|
||||
} else ()
|
||||
setInitialized(true)
|
||||
setInitializeOption(opt)
|
||||
if (!opt.skipAnalysis.getOrElse(false)) appendExec("collectAnalyses", None)
|
||||
jsonRpcRespond(InitializeResult(serverCapabilities), Some(r.id))
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,10 @@ final class NetworkChannel(
|
|||
case _ => false
|
||||
}
|
||||
|
||||
/** True if this channel should receive broadcast events (logMessage, notifyEvent). Default true for backward compatibility. */
|
||||
private[sbt] def subscribeToAll: Boolean =
|
||||
Option(initializeOption.get).flatMap(_.subscribeToAll).getOrElse(false)
|
||||
|
||||
protected def authenticate(token: String): Boolean = instance.authenticate(token)
|
||||
|
||||
protected def setInitialized(value: Boolean): Unit = initialized = value
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ object RunUtilTest extends Properties:
|
|||
override def tests: List[Test] = List(
|
||||
property("mkWindowTitle formats title correctly", testMkWindowTitle),
|
||||
property("mkWindowTitle handles empty strings", testMkWindowTitleEmpty),
|
||||
property("splitArgs with no -- treats all as app args", testSplitArgsNoDash),
|
||||
property("splitArgs splits at first --", testSplitArgsWithDash),
|
||||
property("splitArgs with only -- gives empty jvm and app args", testSplitArgsOnlyDash),
|
||||
property("splitArgs with -- at start gives empty jvm args", testSplitArgsDashAtStart),
|
||||
property("splitArgs with -- at end gives empty app args", testSplitArgsDashAtEnd),
|
||||
property("splitArgs with multiple -- splits at first only", testSplitArgsMultipleDash),
|
||||
property("splitArgs with empty input", testSplitArgsEmpty),
|
||||
)
|
||||
|
||||
def testMkWindowTitle: Property =
|
||||
|
|
@ -34,4 +41,48 @@ object RunUtilTest extends Properties:
|
|||
yield
|
||||
val result = RunUtil.mkWindowTitle("run", "", "", "")
|
||||
Result.assert(result == "sbt run: % % ")
|
||||
|
||||
def testSplitArgsNoDash: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("arg1", "arg2"))
|
||||
Result.assert(jvm.isEmpty).and(Result.assert(app == Seq("arg1", "arg2")))
|
||||
|
||||
def testSplitArgsWithDash: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "-ea", "--", "arg1", "arg2"))
|
||||
Result.assert(jvm == Seq("-Xmx2G", "-ea")).and(Result.assert(app == Seq("arg1", "arg2")))
|
||||
|
||||
def testSplitArgsOnlyDash: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("--"))
|
||||
Result.assert(jvm.isEmpty).and(Result.assert(app.isEmpty))
|
||||
|
||||
def testSplitArgsDashAtStart: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("--", "arg1"))
|
||||
Result.assert(jvm.isEmpty).and(Result.assert(app == Seq("arg1")))
|
||||
|
||||
def testSplitArgsDashAtEnd: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "--"))
|
||||
Result.assert(jvm == Seq("-Xmx2G")).and(Result.assert(app.isEmpty))
|
||||
|
||||
def testSplitArgsMultipleDash: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq("-Xmx2G", "--", "arg1", "--", "arg2"))
|
||||
Result
|
||||
.assert(jvm == Seq("-Xmx2G"))
|
||||
.and(Result.assert(app == Seq("arg1", "--", "arg2")))
|
||||
|
||||
def testSplitArgsEmpty: Property =
|
||||
for _ <- Gen.constant(()).forAll
|
||||
yield
|
||||
val (jvm, app) = RunUtil.splitArgs(Seq.empty)
|
||||
Result.assert(jvm.isEmpty).and(Result.assert(app.isEmpty))
|
||||
end RunUtilTest
|
||||
|
|
|
|||
|
|
@ -11,23 +11,25 @@ package sbt.internal.protocol
|
|||
final class InitializeOption private (
|
||||
val token: Option[String],
|
||||
val skipAnalysis: Option[Boolean],
|
||||
val canWork: Option[Boolean]) extends Serializable {
|
||||
val canWork: Option[Boolean],
|
||||
val subscribeToAll: Option[Boolean]) extends Serializable {
|
||||
|
||||
private def this(token: Option[String]) = this(token, None, None)
|
||||
private def this(token: Option[String], skipAnalysis: Option[Boolean]) = this(token, skipAnalysis, None)
|
||||
private def this(token: Option[String]) = this(token, None, None, None)
|
||||
private def this(token: Option[String], skipAnalysis: Option[Boolean]) = this(token, skipAnalysis, None, None)
|
||||
private def this(token: Option[String], skipAnalysis: Option[Boolean], canWork: Option[Boolean]) = this(token, skipAnalysis, canWork, None)
|
||||
|
||||
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
|
||||
case x: InitializeOption => (this.token == x.token) && (this.skipAnalysis == x.skipAnalysis) && (this.canWork == x.canWork)
|
||||
case x: InitializeOption => (this.token == x.token) && (this.skipAnalysis == x.skipAnalysis) && (this.canWork == x.canWork) && (this.subscribeToAll == x.subscribeToAll)
|
||||
case _ => false
|
||||
})
|
||||
override def hashCode: Int = {
|
||||
37 * (37 * (37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##) + skipAnalysis.##) + canWork.##)
|
||||
37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##) + skipAnalysis.##) + canWork.##) + subscribeToAll.##)
|
||||
}
|
||||
override def toString: String = {
|
||||
"InitializeOption(" + token + ", " + skipAnalysis + ", " + canWork + ")"
|
||||
"InitializeOption(" + token + ", " + skipAnalysis + ", " + canWork + ", " + subscribeToAll + ")"
|
||||
}
|
||||
private def copy(token: Option[String] = token, skipAnalysis: Option[Boolean] = skipAnalysis, canWork: Option[Boolean] = canWork): InitializeOption = {
|
||||
new InitializeOption(token, skipAnalysis, canWork)
|
||||
private def copy(token: Option[String] = token, skipAnalysis: Option[Boolean] = skipAnalysis, canWork: Option[Boolean] = canWork, subscribeToAll: Option[Boolean] = subscribeToAll): InitializeOption = {
|
||||
new InitializeOption(token, skipAnalysis, canWork, subscribeToAll)
|
||||
}
|
||||
def withToken(token: Option[String]): InitializeOption = {
|
||||
copy(token = token)
|
||||
|
|
@ -47,6 +49,12 @@ final class InitializeOption private (
|
|||
def withCanWork(canWork: Boolean): InitializeOption = {
|
||||
copy(canWork = Option(canWork))
|
||||
}
|
||||
def withSubscribeToAll(subscribeToAll: Option[Boolean]): InitializeOption = {
|
||||
copy(subscribeToAll = subscribeToAll)
|
||||
}
|
||||
def withSubscribeToAll(subscribeToAll: Boolean): InitializeOption = {
|
||||
copy(subscribeToAll = Option(subscribeToAll))
|
||||
}
|
||||
}
|
||||
object InitializeOption {
|
||||
|
||||
|
|
@ -56,4 +64,6 @@ object InitializeOption {
|
|||
def apply(token: String, skipAnalysis: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis))
|
||||
def apply(token: Option[String], skipAnalysis: Option[Boolean], canWork: Option[Boolean]): InitializeOption = new InitializeOption(token, skipAnalysis, canWork)
|
||||
def apply(token: String, skipAnalysis: Boolean, canWork: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis), Option(canWork))
|
||||
def apply(token: Option[String], skipAnalysis: Option[Boolean], canWork: Option[Boolean], subscribeToAll: Option[Boolean]): InitializeOption = new InitializeOption(token, skipAnalysis, canWork, subscribeToAll)
|
||||
def apply(token: String, skipAnalysis: Boolean, canWork: Boolean, subscribeToAll: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis), Option(canWork), Option(subscribeToAll))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ given InitializeOptionFormat: JsonFormat[sbt.internal.protocol.InitializeOption]
|
|||
val token = unbuilder.readField[Option[String]]("token")
|
||||
val skipAnalysis = unbuilder.readField[Option[Boolean]]("skipAnalysis")
|
||||
val canWork = unbuilder.readField[Option[Boolean]]("canWork")
|
||||
val subscribeToAll = unbuilder.readField[Option[Boolean]]("subscribeToAll")
|
||||
unbuilder.endObject()
|
||||
sbt.internal.protocol.InitializeOption(token, skipAnalysis, canWork)
|
||||
sbt.internal.protocol.InitializeOption(token, skipAnalysis, canWork, subscribeToAll)
|
||||
case None =>
|
||||
deserializationError("Expected JsObject but found None")
|
||||
}
|
||||
|
|
@ -25,6 +26,7 @@ given InitializeOptionFormat: JsonFormat[sbt.internal.protocol.InitializeOption]
|
|||
builder.addField("token", obj.token)
|
||||
builder.addField("skipAnalysis", obj.skipAnalysis)
|
||||
builder.addField("canWork", obj.canWork)
|
||||
builder.addField("subscribeToAll", obj.subscribeToAll)
|
||||
builder.endObject()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ type InitializeOption {
|
|||
token: String
|
||||
skipAnalysis: Boolean @since("1.4.0")
|
||||
canWork: Boolean @since("1.10.8")
|
||||
subscribeToAll: Boolean @since("2.0.0")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
scalaVersion := "3.6.4"
|
||||
run / fork := true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
object Main {
|
||||
def main(args: Array[String]): Unit = {
|
||||
val prop = System.getProperty("test.prop")
|
||||
assert(prop == "hello", s"Expected system property test.prop=hello but got '$prop'")
|
||||
assert(args.toList == List("arg1", "arg2"), s"Expected args [arg1, arg2] but got ${args.toList}")
|
||||
println("OK")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Verify that JVM args can be passed via the -- delimiter
|
||||
> run -Dtest.prop=hello -- arg1 arg2
|
||||
|
||||
# Verify runMain also supports JVM args via --
|
||||
> runMain Main -Dtest.prop=hello -- arg1 arg2
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
name := "client-subscription"
|
||||
scalaVersion := "2.12.19"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# Exercise server client path (subscribe-to-all by default). Closes #4399.
|
||||
> show name
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package testpkg
|
||||
|
||||
import scala.concurrent.duration.*
|
||||
|
||||
class ClientSubscriptionTest extends AbstractServerTest {
|
||||
override val testDirectory: String = "handshake"
|
||||
|
||||
test("subscribe-to-all (default) client receives broadcast build/logMessage when command runs") {
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": 2, "method": "sbt/exec", "params": { "commandLine": "show name" } }"""
|
||||
)
|
||||
def isLogMessageNotification(line: String): Boolean =
|
||||
line.contains("\"method\":\"build/logMessage\"") || line.contains(
|
||||
"\"method\": \"build/logMessage\""
|
||||
)
|
||||
assert(
|
||||
svr.waitForString(10.seconds)(isLogMessageNotification),
|
||||
"subscribe-to-all client must receive broadcast build/logMessage when a command produces log output"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ClientNoSubscriptionTest extends AbstractServerTest {
|
||||
override val testDirectory: String = "handshake"
|
||||
override def subscribeToAllForTest: Boolean = false
|
||||
|
||||
test("non-subscribed client receives build/logMessage for its own command") {
|
||||
svr.sendJsonRpc(
|
||||
"""{ "jsonrpc": "2.0", "id": 2, "method": "sbt/exec", "params": { "commandLine": "show name" } }"""
|
||||
)
|
||||
def isLogMessageNotification(line: String): Boolean =
|
||||
line.contains("\"method\":\"build/logMessage\"") || line.contains(
|
||||
"\"method\": \"build/logMessage\""
|
||||
)
|
||||
assert(
|
||||
svr.waitForString(10.seconds)(isLogMessageNotification),
|
||||
"non-subscribed client must still receive build/logMessage for its own command"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ trait AbstractServerTest extends AnyFunSuite with BeforeAndAfterAll {
|
|||
var svr: TestServer = scala.compiletime.uninitialized
|
||||
def testDirectory: String
|
||||
def testPath: Path = temp.toPath.resolve(testDirectory)
|
||||
def subscribeToAllForTest: Boolean = true
|
||||
|
||||
private val targetDir: File = {
|
||||
val p0 = new File("..").getAbsoluteFile.getCanonicalFile / "target"
|
||||
|
|
@ -48,7 +49,14 @@ trait AbstractServerTest extends AnyFunSuite with BeforeAndAfterAll {
|
|||
val classpath = TestProperties.classpath.split(File.pathSeparator).map(new File(_))
|
||||
val sbtVersion = TestProperties.version
|
||||
val scalaVersion = TestProperties.scalaVersion
|
||||
svr = TestServer.get(testDirectory, scalaVersion, sbtVersion, classpath.toSeq, temp)
|
||||
svr = TestServer.get(
|
||||
testDirectory,
|
||||
scalaVersion,
|
||||
sbtVersion,
|
||||
classpath.toSeq,
|
||||
temp,
|
||||
subscribeToAllForTest
|
||||
)
|
||||
}
|
||||
override protected def afterAll(): Unit = {
|
||||
svr.bye()
|
||||
|
|
@ -71,13 +79,14 @@ object TestServer {
|
|||
scalaVersion: String,
|
||||
sbtVersion: String,
|
||||
classpath: Seq[File],
|
||||
temp: File
|
||||
temp: File,
|
||||
subscribeToAll: Boolean = true
|
||||
): TestServer = {
|
||||
println(s"Starting test server $testBuild")
|
||||
IO.copyDirectory(serverTestBase / testBuild, temp / testBuild)
|
||||
|
||||
// Each test server instance will be executed in a Thread pool separated from the tests
|
||||
val testServer = TestServer(temp / testBuild, scalaVersion, sbtVersion, classpath)
|
||||
val testServer =
|
||||
TestServer(temp / testBuild, scalaVersion, sbtVersion, classpath, subscribeToAll)
|
||||
// checking last log message after initialization
|
||||
// if something goes wrong here the communication streams are corrupted, restarting
|
||||
val init =
|
||||
|
|
@ -155,7 +164,8 @@ case class TestServer(
|
|||
baseDirectory: File,
|
||||
scalaVersion: String,
|
||||
sbtVersion: String,
|
||||
classpath: Seq[File]
|
||||
classpath: Seq[File],
|
||||
subscribeToAll: Boolean = true
|
||||
) {
|
||||
import TestServer.hostLog
|
||||
|
||||
|
|
@ -227,8 +237,11 @@ case class TestServer(
|
|||
}
|
||||
|
||||
// initiate handshake
|
||||
private val initOptions =
|
||||
if subscribeToAll then """{ "skipAnalysis": true, "canWork": true }"""
|
||||
else """{ "skipAnalysis": true, "canWork": true, "subscribeToAll": false }"""
|
||||
sendJsonRpc(
|
||||
s"""{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { "skipAnalysis": true, "canWork": true } } }"""
|
||||
s"""{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": $initOptions } }"""
|
||||
)
|
||||
|
||||
def test(f: TestServer => Future[Unit]): Future[Unit] = f(this)
|
||||
|
|
|
|||
Loading…
Reference in New Issue