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.
This commit is contained in:
Ethan Atkins 2019-06-02 11:48:39 -07:00
parent 3c81226ba9
commit 7948408368
9 changed files with 43 additions and 41 deletions

View File

@ -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)

View File

@ -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](

View File

@ -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,

View File

@ -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")

View File

@ -40,7 +40,7 @@ object Build {
watchOnFileInputEvent := { (_, _) =>
Watch.CancelWatch
},
Compile / compile / watchOnIteration := { _ =>
Compile / compile / watchOnIteration := { (_, _, _) =>
Watch.CancelWatch
},
checkTriggers := {

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -24,6 +24,6 @@ object Build {
IO.touch(baseDirectory.value / "foo.txt", true)
Some("watching")
},
watchOnIteration := { _ => Watch.CancelWatch }
watchOnIteration := { (_, _, _) => Watch.CancelWatch }
)
}