diff --git a/main-command/src/main/scala/sbt/internal/CommandChannel.scala b/main-command/src/main/scala/sbt/internal/CommandChannel.scala index 49f139a93..7d9eb94d1 100644 --- a/main-command/src/main/scala/sbt/internal/CommandChannel.scala +++ b/main-command/src/main/scala/sbt/internal/CommandChannel.scala @@ -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]) diff --git a/main/src/main/scala/sbt/BackgroundJobService.scala b/main/src/main/scala/sbt/BackgroundJobService.scala index a9189d334..e268b96ce 100644 --- a/main/src/main/scala/sbt/BackgroundJobService.scala +++ b/main/src/main/scala/sbt/BackgroundJobService.scala @@ -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) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 39991d5b2..5564409b7 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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( diff --git a/main/src/main/scala/sbt/EvaluateTask.scala b/main/src/main/scala/sbt/EvaluateTask.scala index ecba40dd5..fed53f3e6 100644 --- a/main/src/main/scala/sbt/EvaluateTask.scala +++ b/main/src/main/scala/sbt/EvaluateTask.scala @@ -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 { diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 610faebb1..9ac7b24aa 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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) diff --git a/main/src/main/scala/sbt/internal/Aggregation.scala b/main/src/main/scala/sbt/internal/Aggregation.scala index e5e0e71e1..221ff1095 100644 --- a/main/src/main/scala/sbt/internal/Aggregation.scala +++ b/main/src/main/scala/sbt/internal/Aggregation.scala @@ -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]], diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 637270ef3..8569bb474 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -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 = { diff --git a/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala b/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala index 8caad1d72..0380f86b0 100644 --- a/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala +++ b/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala @@ -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 { diff --git a/sbt-app/src/sbt-test/actions/add-alias/build.sbt b/sbt-app/src/sbt-test/actions/add-alias/build.sbt index 089e388c1..c8673eaea 100644 --- a/sbt-app/src/sbt-test/actions/add-alias/build.sbt +++ b/sbt-app/src/sbt-test/actions/add-alias/build.sbt @@ -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") diff --git a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt index e9449cc48..e92d9b23d 100644 --- a/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt +++ b/sbt-app/src/sbt-test/classloader-cache/jni/build.sbt @@ -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 ) diff --git a/sbt-app/src/sbt-test/dependency-management/aar-packaging/test b/sbt-app/src/sbt-test/dependency-management/aar-packaging/test index 2182f57b0..5efe58689 100644 --- a/sbt-app/src/sbt-test/dependency-management/aar-packaging/test +++ b/sbt-app/src/sbt-test/dependency-management/aar-packaging/test @@ -1,3 +1,3 @@ $ delete output -> run +> runBlock $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test b/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test index 2182f57b0..5efe58689 100644 --- a/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test +++ b/sbt-app/src/sbt-test/dependency-management/exclude-dependencies2/test @@ -1,3 +1,3 @@ $ delete output -> run +> runBlock $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/profiles/test b/sbt-app/src/sbt-test/dependency-management/profiles/test index 2182f57b0..5efe58689 100644 --- a/sbt-app/src/sbt-test/dependency-management/profiles/test +++ b/sbt-app/src/sbt-test/dependency-management/profiles/test @@ -1,3 +1,3 @@ $ delete output -> run +> runBlock $ exists output diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test index 7df93ab6c..8984f6503 100644 --- a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze/test @@ -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 diff --git a/sbt-app/src/sbt-test/dependency-management/url-no-head/test b/sbt-app/src/sbt-test/dependency-management/url-no-head/test index 2182f57b0..5efe58689 100644 --- a/sbt-app/src/sbt-test/dependency-management/url-no-head/test +++ b/sbt-app/src/sbt-test/dependency-management/url-no-head/test @@ -1,3 +1,3 @@ $ delete output -> run +> runBlock $ exists output diff --git a/sbt-app/src/sbt-test/package/resources/test b/sbt-app/src/sbt-test/package/resources/test index 27ee7a65d..83139bf77 100644 --- a/sbt-app/src/sbt-test/package/resources/test +++ b/sbt-app/src/sbt-test/package/resources/test @@ -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 diff --git a/sbt-app/src/sbt-test/source-dependencies/constants/test b/sbt-app/src/sbt-test/source-dependencies/constants/test index ff8815d85..9d448cea2 100644 --- a/sbt-app/src/sbt-test/source-dependencies/constants/test +++ b/sbt-app/src/sbt-test/source-dependencies/constants/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 diff --git a/sbt-app/src/sbt-test/source-dependencies/implicit-search/build.sbt b/sbt-app/src/sbt-test/source-dependencies/implicit-search/build.sbt index 07fe33830..8f5047976 100644 --- a/sbt-app/src/sbt-test/source-dependencies/implicit-search/build.sbt +++ b/sbt-app/src/sbt-test/source-dependencies/implicit-search/build.sbt @@ -1,2 +1 @@ -ThisBuild / scalaVersion := "2.12.17" - +scalaVersion := "2.12.19" diff --git a/sbt-app/src/sbt-test/source-dependencies/implicit-search/test b/sbt-app/src/sbt-test/source-dependencies/implicit-search/test index 18d69f6b8..d7d7190b5 100644 --- a/sbt-app/src/sbt-test/source-dependencies/implicit-search/test +++ b/sbt-app/src/sbt-test/source-dependencies/implicit-search/test @@ -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 diff --git a/sbt-app/src/sbt-test/source-dependencies/java-basic/test b/sbt-app/src/sbt-test/source-dependencies/java-basic/test index 9dac40735..04a3d604d 100644 --- a/sbt-app/src/sbt-test/source-dependencies/java-basic/test +++ b/sbt-app/src/sbt-test/source-dependencies/java-basic/test @@ -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 \ No newline at end of file +> runBlock diff --git a/sbt-app/src/sbt-test/source-dependencies/linearization/test b/sbt-app/src/sbt-test/source-dependencies/linearization/test index 22f17664a..f6c002d7c 100644 --- a/sbt-app/src/sbt-test/source-dependencies/linearization/test +++ b/sbt-app/src/sbt-test/source-dependencies/linearization/test @@ -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 \ No newline at end of file +> runBlock 11 \ No newline at end of file diff --git a/sbt-app/src/sbt-test/source-dependencies/named/test b/sbt-app/src/sbt-test/source-dependencies/named/test index 28f1c58d3..626ddc2f4 100644 --- a/sbt-app/src/sbt-test/source-dependencies/named/test +++ b/sbt-app/src/sbt-test/source-dependencies/named/test @@ -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 diff --git a/sbt-app/src/sbt-test/source-dependencies/specialized/test b/sbt-app/src/sbt-test/source-dependencies/specialized/test index b9e1ad09c..76c2120a8 100644 --- a/sbt-app/src/sbt-test/source-dependencies/specialized/test +++ b/sbt-app/src/sbt-test/source-dependencies/specialized/test @@ -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 \ No newline at end of file +> runBlock true \ No newline at end of file