mirror of https://github.com/sbt/sbt.git
Merge pull request #7688 from eed3si9n/wip/non-blocking-run
[2.x] feat: Non-blocking run
This commit is contained in:
commit
3c645bbeeb
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
$ delete output
|
||||
> run
|
||||
> runBlock
|
||||
$ exists output
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
$ delete output
|
||||
> run
|
||||
> runBlock
|
||||
$ exists output
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
$ delete output
|
||||
> run
|
||||
> runBlock
|
||||
$ exists output
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
$ delete output
|
||||
> run
|
||||
> runBlock
|
||||
$ exists output
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
ThisBuild / scalaVersion := "2.12.17"
|
||||
|
||||
scalaVersion := "2.12.19"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue