From 794840836891b23600569ae2412a84696aa2ea62 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sun, 2 Jun 2019 11:48:39 -0700 Subject: [PATCH] Simplify watch callbacks While writing documentation for the watch subsystem, I realized that it's awkward to configure watch to clear the screen before task evaluation. To make this easier, I added a setting watchBeforeCommand which is an arbitrary function that will run before the watch process evaluates the command(s). I also added helper functions for adding clear screen functionality. I also realized that we weren't using the watchOnEnter or watchOnExit callbacks anywhere. I had added these to support setting up some state before watch starts and cleaning it up before it exits for plugin authors. It makes sense to remove that functionality for 1.3.0 and only if a need presents itself re-add it in a later version of sbt. I also made a few apis private[sbt] that I'm not sure about. Writing documentation made me realize that some of these are redundant and/or not ready for general consumption. --- .../main/scala/sbt/internal/Continuous.scala | 28 ++++++++----------- main/src/main/scala/sbt/nio/Keys.scala | 19 ++++++------- main/src/main/scala/sbt/nio/Watch.scala | 21 ++++++++++---- sbt/src/sbt-test/watch/alias/build.sbt | 2 +- .../project/Build.scala | 2 +- sbt/src/sbt-test/watch/on-change/build.sbt | 2 +- .../watch/on-start-watch/changes/extra.sbt | 2 +- sbt/src/sbt-test/watch/on-start-watch/test | 6 ++-- .../sbt-test/watch/task/changes/Build.scala | 2 +- 9 files changed, 43 insertions(+), 41 deletions(-) diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 6c26587fa..94fe88a4a 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -351,17 +351,17 @@ private[sbt] object Continuous extends DeprecatedContinuous { aggregate(configs, logger, in, s, currentCount, isCommand, commands, fileStampCache) val task = () => { currentCount.getAndIncrement() + callbacks.beforeCommand() // abort as soon as one of the tasks fails valid.takeWhile(_._3.apply()) updateLegacyWatchState(s, configs.flatMap(_.inputs().map(_.glob)), currentCount.get()) () } - callbacks.onEnter() - // Here we enter the Watched.watch state machine. We will not return until one of the - // state machine callbacks returns Watched.CancelWatch, Watched.Custom, Watched.HandleError - // or Watched.ReloadException. The task defined above will be run at least once. It will be run - // additional times whenever the state transition callbacks return Watched.Trigger. try { + // Here we enter the Watched.watch state machine. We will not return until one of the + // state machine callbacks returns Watched.CancelWatch, Watched.Custom, Watched.HandleError + // or Watched.ReloadException. The task defined above will be run at least once. It will be run + // additional times whenever the state transition callbacks return Watched.Trigger. val terminationAction = Watch(task, callbacks.onStart, callbacks.nextEvent) terminationAction match { case e: Watch.HandleUnexpectedError => @@ -427,7 +427,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { private class Callbacks( val nextEvent: () => Watch.Action, - val onEnter: () => Unit, + val beforeCommand: () => Unit, val onExit: () => Unit, val onStart: () => Watch.Action, val onTermination: (Watch.Action, String, Int, State) => State @@ -468,19 +468,16 @@ private[sbt] object Continuous extends DeprecatedContinuous { ): Callbacks = { val project = extracted.currentRef.project val logger = setLevel(rawLogger, configs.map(_.watchSettings.logLevel).min, state) - val onEnter = () => configs.foreach(_.watchSettings.onEnter()) + val beforeCommand = () => configs.foreach(_.watchSettings.beforeCommand()) val onStart: () => Watch.Action = getOnStart(project, commands, configs, rawLogger, count) val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger) val (nextFileEvent, cleanupFileMonitor): (() => Option[(Watch.Event, Watch.Action)], () => Unit) = getFileEvents(configs, rawLogger, state, count, commands, fileStampCache) val nextEvent: () => Watch.Action = combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger) - val onExit = () => { - cleanupFileMonitor() - configs.foreach(_.watchSettings.onExit()) - } + val onExit = () => cleanupFileMonitor() val onTermination = getOnTermination(configs, isCommand) - new Callbacks(nextEvent, onEnter, onExit, onStart, onTermination) + new Callbacks(nextEvent, beforeCommand, onExit, onStart, onTermination) } private def getOnTermination( @@ -509,7 +506,7 @@ private[sbt] object Continuous extends DeprecatedContinuous { val f: () => Seq[Watch.Action] = () => { configs.map { params => val ws = params.watchSettings - ws.onIteration.map(_(count.get)).getOrElse { + ws.onIteration.map(_(count.get, project, commands)).getOrElse { if (configs.size == 1) { // Only allow custom start messages for single tasks ws.startMessage match { case Some(Left(sm)) => logger.info(sm(params.watchState(count.get()))) @@ -900,11 +897,10 @@ private[sbt] object Continuous extends DeprecatedContinuous { val inputParser: Parser[Watch.Action] = key.get(watchInputParser).getOrElse(Watch.defaultInputParser) val logLevel: Level.Value = key.get(watchLogLevel).getOrElse(Level.Info) - val onEnter: () => Unit = key.get(watchOnEnter).getOrElse(() => {}) - val onExit: () => Unit = key.get(watchOnExit).getOrElse(() => {}) + val beforeCommand: () => Unit = key.get(watchBeforeCommand).getOrElse(() => {}) val onFileInputEvent: WatchOnEvent = key.get(watchOnFileInputEvent).getOrElse(Watch.trigger) - val onIteration: Option[Int => Watch.Action] = key.get(watchOnIteration) + val onIteration: Option[(Int, String, Seq[String]) => Watch.Action] = key.get(watchOnIteration) val onTermination: Option[(Watch.Action, String, Int, State) => State] = key.get(watchOnTermination) val startMessage: StartMessage = getStartMessage(key) diff --git a/main/src/main/scala/sbt/nio/Keys.scala b/main/src/main/scala/sbt/nio/Keys.scala index 528cfaabd..b85271515 100644 --- a/main/src/main/scala/sbt/nio/Keys.scala +++ b/main/src/main/scala/sbt/nio/Keys.scala @@ -68,12 +68,12 @@ object Keys { "detected regardless of whether or not the underlying file has actually changed." // watch related keys + val watchBeforeCommand = settingKey[() => Unit]( + "Function to run prior to running a command in a continuous build." + ).withRank(DSetting) val watchForceTriggerOnAnyChange = Def.settingKey[Boolean](forceTriggerOnAnyChangeMessage).withRank(DSetting) - val watchLogLevel = - settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.") - .withRank(DSetting) - val watchInputHandler = settingKey[InputStream => Watch.Action]( + private[sbt] val watchInputHandler = settingKey[InputStream => Watch.Action]( "Function that is periodically invoked to determine if the continuous build should be stopped or if a build should be triggered. It will usually read from stdin to respond to user commands. This is only invoked if watchInputStream is set." ).withRank(DSetting) val watchInputStream = taskKey[InputStream]( @@ -82,16 +82,13 @@ object Keys { val watchInputParser = settingKey[Parser[Watch.Action]]( "A parser of user input that can be used to trigger or exit a continuous build" ).withRank(DSetting) - val watchOnEnter = settingKey[() => Unit]( - "Function to run prior to beginning a continuous build. This will run before the continuous task(s) is(are) first evaluated." - ).withRank(DSetting) - val watchOnExit = settingKey[() => Unit]( - "Function to run upon exit of a continuous build. It can be used to cleanup resources used during the watch." - ).withRank(DSetting) + val watchLogLevel = + settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.") + .withRank(DSetting) val watchOnFileInputEvent = settingKey[(Int, Watch.Event) => Watch.Action]( "Callback to invoke if an event is triggered in a continuous build by one of the files matching an fileInput glob for the task and its transitive dependencies" ).withRank(DSetting) - val watchOnIteration = settingKey[Int => Watch.Action]( + val watchOnIteration = settingKey[(Int, String, Seq[String]) => Watch.Action]( "Function that is invoked before waiting for file system events or user input events." ).withRank(DSetting) val watchOnTermination = settingKey[(Watch.Action, String, Int, State) => State]( diff --git a/main/src/main/scala/sbt/nio/Watch.scala b/main/src/main/scala/sbt/nio/Watch.scala index 745af45a1..9a073f45b 100644 --- a/main/src/main/scala/sbt/nio/Watch.scala +++ b/main/src/main/scala/sbt/nio/Watch.scala @@ -420,7 +420,7 @@ object Watch { /** * Default no-op callback. */ - val defaultOnEnter: () => Unit = () => {} + val defaultBeforeCommand: () => Unit = () => {} private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State = onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination") @@ -473,16 +473,25 @@ object Watch { final val defaultPollInterval: FiniteDuration = 500.milliseconds /** - * A constant function that returns an Option wrapped string that clears the screen when - * written to stdout. + * Clears the console screen when evaluated. */ - final val clearOnTrigger: Int => Option[String] = - ((_: Int) => Some(Watched.clearScreen)).label("Watched.clearOnTrigger") + final val clearScreen: () => Unit = + (() => println("\u001b[2J\u001b[0;0H")).label("Watch.clearScreen") + + /** + * A function that first clears the screen and then returns the default on trigger message. + */ + final val clearScreenOnTrigger: (Int, Path, Seq[String]) => Option[String] = { + (count: Int, path: Path, commands: Seq[String]) => + clearScreen() + defaultOnTriggerMessage(count, path, commands) + }.label("Watch.clearScreenOnTrigger") + private[sbt] def defaults: Seq[Def.Setting[_]] = Seq( sbt.Keys.watchAntiEntropy :== Watch.defaultAntiEntropy, watchAntiEntropyRetentionPeriod :== Watch.defaultAntiEntropyRetentionPeriod, watchLogLevel :== Level.Info, - watchOnEnter :== Watch.defaultOnEnter, + watchBeforeCommand :== Watch.defaultBeforeCommand, watchOnFileInputEvent :== Watch.trigger, watchDeletionQuarantinePeriod :== Watch.defaultDeletionQuarantinePeriod, sbt.Keys.watchService :== Watched.newWatchService, diff --git a/sbt/src/sbt-test/watch/alias/build.sbt b/sbt/src/sbt-test/watch/alias/build.sbt index 0fab94fd9..f1d0d6c0a 100644 --- a/sbt/src/sbt-test/watch/alias/build.sbt +++ b/sbt/src/sbt-test/watch/alias/build.sbt @@ -1,6 +1,6 @@ val foo = taskKey[Unit]("foo") foo := println("foo") -foo / watchOnIteration := { _ => sbt.nio.Watch.CancelWatch } +foo / watchOnIteration := { (_, _, _) => sbt.nio.Watch.CancelWatch } addCommandAlias("bar", "foo") addCommandAlias("baz", "foo") diff --git a/sbt/src/sbt-test/watch/file-input-aggregation/project/Build.scala b/sbt/src/sbt-test/watch/file-input-aggregation/project/Build.scala index 6fe14128f..32b999320 100644 --- a/sbt/src/sbt-test/watch/file-input-aggregation/project/Build.scala +++ b/sbt/src/sbt-test/watch/file-input-aggregation/project/Build.scala @@ -40,7 +40,7 @@ object Build { watchOnFileInputEvent := { (_, _) => Watch.CancelWatch }, - Compile / compile / watchOnIteration := { _ => + Compile / compile / watchOnIteration := { (_, _, _) => Watch.CancelWatch }, checkTriggers := { diff --git a/sbt/src/sbt-test/watch/on-change/build.sbt b/sbt/src/sbt-test/watch/on-change/build.sbt index 3864a2ef0..260e5d1c2 100644 --- a/sbt/src/sbt-test/watch/on-change/build.sbt +++ b/sbt/src/sbt-test/watch/on-change/build.sbt @@ -10,7 +10,7 @@ watchTriggeredMessage := { (i, path: Path, c) => prev(i, path, c) } -watchOnIteration := { i: Int => +watchOnIteration := { (i: Int, _, _) => val base = baseDirectory.value.toPath val src = base.resolve("src").resolve("main").resolve("scala").resolve("sbt").resolve("test") diff --git a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt index 4c4c95a65..85c2f3c85 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt @@ -1,7 +1,7 @@ val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded") checkReloaded := { () } -watchOnIteration := { _ => sbt.nio.Watch.CancelWatch } +watchOnIteration := { (_, _, _) => sbt.nio.Watch.CancelWatch } Compile / compile := { Count.increment() diff --git a/sbt/src/sbt-test/watch/on-start-watch/test b/sbt/src/sbt-test/watch/on-start-watch/test index 905322c07..f796f7a13 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/test +++ b/sbt/src/sbt-test/watch/on-start-watch/test @@ -3,19 +3,19 @@ # verify that the watch terminates when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore } +> set watchOnIteration := { (count: Int, _, _) => if (count == 2) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore } > ~compile > checkCount 2 # verify that the watch terminates and returns an error when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) new sbt.nio.Watch.HandleError(new Exception("")) else sbt.nio.Watch.Ignore } +> set watchOnIteration := { (count: Int, _, _) => if (count == 2) new sbt.nio.Watch.HandleError(new Exception("")) else sbt.nio.Watch.Ignore } # Returning Watch.HandleError causes the '~' command to fail -> ~compile > checkCount 2 # verify that a re-build is triggered when we reach the specified count > resetCount -> set watchOnIteration := { (count: Int) => if (count == 2) sbt.nio.Watch.Trigger else if (count == 3) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore } +> set watchOnIteration := { (count: Int, _, _) => if (count == 2) sbt.nio.Watch.Trigger else if (count == 3) sbt.nio.Watch.CancelWatch else sbt.nio.Watch.Ignore } > ~compile > checkCount 3 diff --git a/sbt/src/sbt-test/watch/task/changes/Build.scala b/sbt/src/sbt-test/watch/task/changes/Build.scala index 5478afedf..28a790802 100644 --- a/sbt/src/sbt-test/watch/task/changes/Build.scala +++ b/sbt/src/sbt-test/watch/task/changes/Build.scala @@ -24,6 +24,6 @@ object Build { IO.touch(baseDirectory.value / "foo.txt", true) Some("watching") }, - watchOnIteration := { _ => Watch.CancelWatch } + watchOnIteration := { (_, _, _) => Watch.CancelWatch } ) }