Merge pull request #7688 from eed3si9n/wip/non-blocking-run

[2.x] feat: Non-blocking run
This commit is contained in:
eugene yokota 2024-10-01 16:47:26 -04:00 committed by GitHub
commit 3c645bbeeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 113 additions and 51 deletions

View File

@ -101,6 +101,10 @@ abstract class CommandChannel {
}
private[sbt] def terminal: Terminal
private[sbt] var _active: Boolean = true
private[sbt] def pause(): Unit = _active = false
private[sbt] def isPaused: Boolean = !_active
private[sbt] def resume(): Unit = _active = true
}
// case class Exec(commandLine: String, source: Option[CommandSource])

View File

@ -86,6 +86,8 @@ abstract class BackgroundJobService extends Closeable {
hashContents: Boolean,
converter: FileConverter,
): Classpath = copyClasspath(products, full, workingDirectory, converter)
private[sbt] def pauseChannelDuringJob(state: State, handle: JobHandle): Unit
}
object BackgroundJobService {
@ -108,3 +110,10 @@ abstract class JobHandle {
def humanReadableName: String
def spawningTask: ScopedKey[_]
}
/**
* This datatype is used signal the task engine or the commands
* that the background job is emulated to be a foreground job on
* the originating channel.
*/
case class EmulateForeground(handle: JobHandle)

View File

@ -1051,6 +1051,12 @@ object Defaults extends BuildCommon {
},
runMain := foregroundRunMainTask.evaluated,
run := foregroundRunTask.evaluated,
runBlock := {
val r = run.evaluated
val service = bgJobService.value
service.waitForTry(r.handle).get
r
},
fgRun := runTask(fullClasspath, (run / mainClass), (run / runner)).evaluated,
fgRunMain := runMainTask(fullClasspath, (run / runner)).evaluated,
copyResources := copyResourcesTask.value,
@ -2143,21 +2149,18 @@ object Defaults extends BuildCommon {
}
}
// runMain calls bgRunMain in the background and waits for the result.
def foregroundRunMainTask: Initialize[InputTask[Unit]] =
Def.inputTask[Unit] {
// `runMain` calls bgRunMain in the background and pauses the current channel
def foregroundRunMainTask: Initialize[InputTask[EmulateForeground]] =
Def.inputTask {
val handle = bgRunMain.evaluated
val service = bgJobService.value
service.waitForTry(handle).get
()
EmulateForeground(handle)
}
// run calls bgRun in the background and waits for the result.
def foregroundRunTask: Initialize[InputTask[Unit]] =
// `run` task calls bgRun in the background and pauses the current channel
def foregroundRunTask: Initialize[InputTask[EmulateForeground]] =
Def.inputTask {
val handle = bgRun.evaluated
val service = bgJobService.value
service.waitForTry(handle).get
EmulateForeground(handle)
}
def runMainTask(

View File

@ -512,6 +512,16 @@ object EvaluateTask {
case Some(t: Task[?]) => transformNode(t).isEmpty
case _ => true
}
def suspendChannel[A1](
state: State,
result: Result[A1]
): Unit =
(state.getSetting(Global / Keys.bgJobService), result) match
case (Some(service), Result.Value(List(KeyValue(_, EmulateForeground(handle))))) =>
state.remainingCommands match
case Nil => service.waitForTry(handle).get
case _ => service.pauseChannelDuringJob(state, handle)
case _ => ()
def run() = {
val x = new Execute(
Execute.config(config.checkCycles, overwriteNode),
@ -529,6 +539,7 @@ object EvaluateTask {
} finally shutdown()
val replaced = transformInc(result)
logIncResult(replaced, state, streams)
suspendChannel(newState, replaced)
(newState, replaced)
}
object runningEngine extends RunningTaskEngine {

View File

@ -318,8 +318,9 @@ object Keys {
// Run Keys
val selectMainClass = taskKey[Option[String]]("Selects the main class to run.").withRank(BMinusTask)
val mainClass = taskKey[Option[String]]("Defines the main class for packaging or running.").withRank(BPlusTask)
val run = inputKey[Unit]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask)
val runMain = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val run = inputKey[EmulateForeground]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask)
val runBlock = inputKey[EmulateForeground]("Runs a main class, and blocks until it's done.").withRank(DTask)
val runMain = inputKey[EmulateForeground]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask)
val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask)
val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask)
val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting)

View File

@ -80,13 +80,25 @@ object Aggregation {
val success = results match
case Result.Value(_) => true
case Result.Inc(_) => false
val isPaused = currentChannel(state) match
case Some(channel) => channel.isPaused
case None => false
results.toEither.foreach { r =>
if show.taskValues then printSettings(r, show.print) else ()
}
if show.success && !state.get(suppressShow).getOrElse(false) then
if !isPaused && show.success && !state.get(suppressShow).getOrElse(false) then
printSuccess(start, stop, extracted, success, cacheSummary, log)
else ()
private def currentChannel(state: State): Option[CommandChannel] =
state.currentCommand match
case Some(exec) =>
exec.source match
case Some(source) =>
StandardMain.exchange.channels.find(_.name == source.channelName)
case _ => None
case _ => None
def timedRun[A](
s: State,
ts: Values[Task[A]],

View File

@ -146,10 +146,11 @@ private[sbt] final class CommandExchange {
}
private def addConsoleChannel(): Unit =
if (!Terminal.startedByRemoteClient) {
if Terminal.startedByRemoteClient then ()
else
val name = ConsoleChannel.defaultName
subscribe(new ConsoleChannel(name, mkAskUser(name)))
}
def run(s: State): State = run(s, s.get(autoStartServer).getOrElse(true))
def run(s: State, autoStart: Boolean): State = {
if (autoStartServerSysProp && autoStart) runServer(s)
@ -376,13 +377,14 @@ private[sbt] final class CommandExchange {
private[sbt] def setExec(exec: Option[Exec]): Unit = currentExecRef.set(exec.orNull)
def prompt(event: ConsolePromptEvent): Unit = {
def prompt(event: ConsolePromptEvent): Unit =
currentExecRef.set(null)
channels.foreach {
case c if ContinuousCommands.isInWatch(lastState.get, c) =>
case c => c.prompt(event)
case c =>
if c.isPaused then ()
else c.prompt(event)
}
}
def unprompt(event: ConsoleUnpromptEvent): Unit = channels.foreach(_.unprompt(event))
def logMessage(event: LogEvent): Unit = {

View File

@ -24,7 +24,7 @@ import sbt.internal.inc.classpath.ClasspathFilter
import sbt.internal.util.{ Attributed, ManagedLogger }
import sbt.io.syntax._
import sbt.io.{ Hash, IO }
import sbt.util.Logger
import sbt.util.{ Level, Logger }
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
@ -79,6 +79,9 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe
private val nextId = new AtomicLong(1)
private val pool = new BackgroundThreadPool()
private val context = LoggerContext()
// EC for onStop handler below
given ExecutionContext =
ExecutionContext.fromExecutor(pool.executor)
private[sbt] def serviceTempDirBase: File
private[sbt] def useLog4J: Boolean
@ -119,10 +122,6 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe
val workingDirectory: File,
val job: BackgroundJob
) extends AbstractJobHandle {
// EC for onStop handler below
implicit val executionContext: ExecutionContext =
ExecutionContext.fromExecutor(pool.executor)
def humanReadableName: String = job.humanReadableName
job.onStop { () =>
@ -306,6 +305,28 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe
converter: FileConverter,
): Classpath =
copyClasspath(products, full, workingDirectory, hashFileContents = true, converter)
private[sbt] def pauseChannelDuringJob(state: State, handle: JobHandle): Unit =
currentChannel(state) match
case Some(channel) =>
handle match
case t: ThreadJobHandle =>
val level = channel.logLevel
channel.setLevel(Level.Error)
channel.pause()
t.job.onStop: () =>
channel.setLevel(level)
channel.resume()
channel.prompt(ConsolePromptEvent(state))
case _ => ()
case _ => ()
private[sbt] def currentChannel(state: State): Option[CommandChannel] =
state.currentCommand match
case Some(e: Exec) if e.source.isDefined =>
val source = e.source.get
StandardMain.exchange.channelForName(source.channelName)
case _ => None
}
private[sbt] object BackgroundThreadPool {

View File

@ -1,3 +1,3 @@
addCommandAlias("demo-success", "run true")
addCommandAlias("demo-failure", "run false")
addCommandAlias("demo-success", "runBlock true")
addCommandAlias("demo-failure", "runBlock false")
addCommandAlias("z", "scalaVersion")

View File

@ -7,7 +7,7 @@ val dropLibraryPath = taskKey[Unit]("Drop the last path from the java.library.pa
val wrappedRun = taskKey[Unit]("Run with modified java.library.path")
val wrappedTest = taskKey[Unit]("Test with modified java.library.path")
def wrap(task: InputKey[Unit]): Def.Initialize[Task[Unit]] =
def wrap[A1](task: InputKey[A1]): Def.Initialize[Task[Unit]] =
Def.sequential(appendToLibraryPath, task.toTask(""), dropLibraryPath)
// ThisBuild / turbo := true
@ -35,6 +35,6 @@ val root = (project in file(".")).settings(
val cp = System.getProperty("java.library.path", "").split(":").dropRight(1)
System.setProperty("java.library.path", cp.mkString(":"))
},
wrappedRun := wrap(Runtime / run).value,
wrappedRun := wrap(Runtime / runBlock).value,
wrappedTest := wrap(Test / testOnly).value
)

View File

@ -1,3 +1,3 @@
$ delete output
> run
> runBlock
$ exists output

View File

@ -1,3 +1,3 @@
$ delete output
> run
> runBlock
$ exists output

View File

@ -1,3 +1,3 @@
$ delete output
> run
> runBlock
$ exists output

View File

@ -1,12 +1,12 @@
> a/checkLibs
> b/checkLibs
> b/run
> b/runBlock
$ exists s2.13.8.txt
$ delete s2.13.8.txt
# don't crash when expanding the macro
> b3/run
> b3/runBlock
$ exists s2.13.10.txt
$ delete s2.13.10.txt

View File

@ -1,3 +1,3 @@
$ delete output
> run
> runBlock
$ exists output

View File

@ -4,7 +4,7 @@
# This should fail because the Main object is in package jartest and the resource is directly
# in src/main/resources
-> run
-> runBlock
> package
@ -18,7 +18,7 @@ $ copy-file src/main/resources/main_resource_test src/main/resources/jartest/mai
$ delete src/main/resources/main_resource_test
# This should succeed because sbt should put the resource on the runClasspath
> run
> runBlock
# This is necessary because package bases whether or not to run on last modified times, which don't have
# high enough resolution to notice the above move of main_resource_test

View File

@ -3,14 +3,14 @@
$ copy-file changes/B.scala B.scala
$ copy-file changes/A1.scala A.scala
> run 1
> runBlock 1
$ copy-file changes/A2.scala A.scala
> run 2
> runBlock 2
> clean
> ++2.13.12!
$ copy-file changes/A1.scala A.scala
> run 1
> runBlock 1
$ copy-file changes/A2.scala A.scala
> run 2
> runBlock 2

View File

@ -1,2 +1 @@
ThisBuild / scalaVersion := "2.12.17"
scalaVersion := "2.12.19"

View File

@ -2,10 +2,10 @@ $ copy-file changes/A1.scala A.scala
$ copy-file changes/B.scala B.scala
$ copy-file changes/C.scala C.scala
> compile
-> run
-> runBlock
$ copy-file changes/A2.scala A.scala
$ sleep 1000
> compile
> run
> runBlock

View File

@ -36,10 +36,10 @@ $ delete src/main/java/a/A.java
# It shouldn't run though, because it doesn't have a main method
$ copy-file changes/B1.java src/main/java/a/b/B.java
> compile
-> run
-> runBlock
# Replace B with a new B that has a main method and should therefore run
# if the main method was properly detected
$ copy-file changes/B3.java src/main/java/a/b/B.java
> run
> runBlock

View File

@ -1,7 +1,7 @@
> compile
# the value of F.x should be 16
> run 16
> runBlock 16
# modify D.scala so that the linearization changes
$ copy-file changes/D.scala D.scala
@ -12,4 +12,4 @@ $ sleep 1000
# if F is recompiled, the value of x should be 11, otherwise it will still be 16
# and this will fail
> run 11
> runBlock 11

View File

@ -4,7 +4,7 @@
> compile
# result should be 1
> run 1
> runBlock 1
# change order of arguments in A.x
$ copy-file changes/A.scala A.scala
@ -13,4 +13,4 @@ $ copy-file changes/A.scala A.scala
> compile
# Should still get 1 and not -1
> run 1
> runBlock 1

View File

@ -2,7 +2,7 @@
> compile
# verify that erased A.x can be called normally and reflectively
> run false
> runBlock false
# make A.x specialized
$ copy-file changes/A.scala A.scala
@ -12,4 +12,4 @@ $ copy-file changes/A.scala A.scala
# verify that specialized A.x can be called normally and reflectively
# NOTE: this test doesn't actually work correctly: have to check the output to see that B.scala was recompiled
> run true
> runBlock true