Create Watch.scala

I decided that it makes sense to move all of the new watch code out of
the Watched companion object since the Watched trait itself is now
deprecated. I don't really like having the new code in Watched.scala
mixed with the legacy code, so I pulled it all out and moved it into the
Watch object. Since we have to put all of the logic for the Continuous
object in main in order to access the sbt.Keys object, it makes sense to
move the logic out of main-command and into command so that most of the
watch related logic is in the same subproject.
This commit is contained in:
Ethan Atkins 2019-03-27 20:42:10 -07:00
parent e868c43fcc
commit 40d8d8876d
22 changed files with 563 additions and 558 deletions

View File

@ -7,25 +7,18 @@
package sbt package sbt
import java.io.{ File, InputStream } import java.io.File
import java.nio.file.FileSystems import java.nio.file.FileSystems
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.internal.LabeledFunctions._ import sbt.internal.LabeledFunctions._
import sbt.internal.{ FileAttributes, LegacyWatched } import sbt.internal.LegacyWatched
import sbt.internal.io.{ EventMonitor, Source, WatchState } import sbt.internal.io.{ EventMonitor, Source, WatchState }
import sbt.internal.util.Types.const import sbt.internal.util.Types.const
import sbt.internal.util.complete.DefaultParsers._ import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.Parser
import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update }
import sbt.io._ import sbt.io._
import sbt.util.{ Level, Logger }
import scala.annotation.tailrec
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Properties import scala.util.Properties
import scala.util.control.NonFatal
@deprecated("Watched is no longer used to implement continuous execution", "1.3.0") @deprecated("Watched is no longer used to implement continuous execution", "1.3.0")
trait Watched { trait Watched {
@ -58,283 +51,13 @@ trait Watched {
object Watched { object Watched {
/**
* This trait is used to communicate what the watch should do next at various points in time. It
* is heavily linked to a number of callbacks in [[WatchConfig]]. For example, when the event
* monitor detects a changed source we expect [[WatchConfig.onWatchEvent]] to return [[Trigger]].
*/
sealed trait Action
/**
* Provides a default Ordering for actions. Lower values correspond to higher priority actions.
* [[CancelWatch]] is higher priority than [[ContinueWatch]].
*/
object Action {
implicit object ordering extends Ordering[Action] {
override def compare(left: Action, right: Action): Int = (left, right) match {
case (a: ContinueWatch, b: ContinueWatch) => ContinueWatch.ordering.compare(a, b)
case (_: ContinueWatch, _: CancelWatch) => 1
case (a: CancelWatch, b: CancelWatch) => CancelWatch.ordering.compare(a, b)
case (_: CancelWatch, _: ContinueWatch) => -1
}
}
}
/**
* Action that indicates that the watch should stop.
*/
sealed trait CancelWatch extends Action
/**
* Action that does not terminate the watch but might trigger a build.
*/
sealed trait ContinueWatch extends Action
/**
* Provides a default Ordering for classes extending [[ContinueWatch]]. [[Trigger]] is higher
* priority than [[Ignore]].
*/
object ContinueWatch {
/**
* A default [[Ordering]] for [[ContinueWatch]]. [[Trigger]] is higher priority than [[Ignore]].
*/
implicit object ordering extends Ordering[ContinueWatch] {
override def compare(left: ContinueWatch, right: ContinueWatch): Int = left match {
case Ignore => if (right == Ignore) 0 else 1
case Trigger => if (right == Trigger) 0 else -1
}
}
}
/**
* Action that indicates that the watch should stop.
*/
case object CancelWatch extends CancelWatch {
/**
* A default [[Ordering]] for [[ContinueWatch]]. The priority of each type of [[CancelWatch]]
* is reflected by the ordering of the case statements in the [[ordering.compare]] method,
* e.g. [[Custom]] is higher priority than [[HandleError]].
*/
implicit object ordering extends Ordering[CancelWatch] {
override def compare(left: CancelWatch, right: CancelWatch): Int = left match {
// Note that a negative return value means the left CancelWatch is preferred to the right
// CancelWatch while the inverse is true for a positive return value. This logic could
// likely be simplified, but the pattern matching approach makes it very clear what happens
// for each type of Action.
case _: Custom =>
right match {
case _: Custom => 0
case _ => -1
}
case _: HandleError =>
right match {
case _: Custom => 1
case _: HandleError => 0
case _ => -1
}
case _: Run =>
right match {
case _: Run => 0
case CancelWatch | Reload => -1
case _ => 1
}
case CancelWatch =>
right match {
case CancelWatch => 0
case Reload => -1
case _ => 1
}
case Reload => if (right == Reload) 0 else 1
}
}
}
/**
* Action that indicates that an error has occurred. The watch will be terminated when this action
* is produced.
*/
final class HandleError(val throwable: Throwable) extends CancelWatch {
override def equals(o: Any): Boolean = o match {
case that: HandleError => this.throwable == that.throwable
case _ => false
}
override def hashCode: Int = throwable.hashCode
override def toString: String = s"HandleError($throwable)"
}
/**
* Action that indicates that the watch should continue as though nothing happened. This may be
* because, for example, no user input was yet available.
*/
case object Ignore extends ContinueWatch
/**
* Action that indicates that the watch should pause while the build is reloaded. This is used to
* automatically reload the project when the build files (e.g. build.sbt) are changed.
*/
case object Reload extends CancelWatch
/**
* Action that indicates that we should exit and run the provided command.
* @param commands the commands to run after we exit the watch
*/
final class Run(val commands: String*) extends CancelWatch {
override def toString: String = s"Run(${commands.mkString(", ")})"
}
// For now leave this private in case this isn't the best unapply type signature since it can't
// be evolved in a binary compatible way.
private object Run {
def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None)))
}
/**
* Action that indicates that the watch process should re-run the command.
*/
case object Trigger extends ContinueWatch
/**
* A user defined Action. It is not sealed so that the user can create custom instances. If any
* of the [[Watched.watch]] callbacks return [[Custom]], then watch will terminate.
*/
trait Custom extends CancelWatch
@deprecated("WatchSource is replaced by sbt.io.Glob", "1.3.0") @deprecated("WatchSource is replaced by sbt.io.Glob", "1.3.0")
type WatchSource = Source type WatchSource = Source
private[sbt] type OnTermination = (Action, String, State) => State
private[sbt] type OnEnter = () => Unit
def terminateWatch(key: Int): Boolean = Watched.isEnter(key) def terminateWatch(key: Int): Boolean = Watched.isEnter(key)
/**
* A constant function that returns [[Trigger]].
*/
final val trigger: (Int, Event[FileAttributes]) => Watched.Action = {
(_: Int, _: Event[FileAttributes]) =>
Trigger
}.label("Watched.trigger")
def ifChanged(action: Action): (Int, Event[FileAttributes]) => Watched.Action =
(_: Int, event: Event[FileAttributes]) =>
event match {
case Update(prev, cur, _) if prev.value != cur.value => action
case _: Creation[_] | _: Deletion[_] => action
case _ => Ignore
}
private[this] val options =
if (Util.isWindows)
"press 'enter' to return to the shell or the following keys followed by 'enter': 'r' to" +
" re-run the command, 'x' to exit sbt"
else "press 'r' to re-run the command, 'x' to exit sbt or 'enter' to return to the shell"
private def waitMessage(project: String): String = private def waitMessage(project: String): String =
s"Waiting for source changes$project... (press enter to interrupt$options)" s"Waiting for source changes$project... (press enter to interrupt)"
/**
* The minimum delay between build triggers for the same file. If the file is detected
* to have changed within this period from the last build trigger, the event will be discarded.
*/
final val defaultAntiEntropy: FiniteDuration = 500.milliseconds
/**
* The duration in wall clock time for which a FileEventMonitor will retain anti-entropy
* events for files. This is an implementation detail of the FileEventMonitor. It should
* hopefully not need to be set by the users. It is needed because when a task takes a long time
* to run, it is possible that events will be detected for the file that triggered the build that
* occur within the anti-entropy period. We still allow it to be configured to limit the memory
* usage of the FileEventMonitor (but this is somewhat unlikely to be a problem).
*/
final val defaultAntiEntropyRetentionPeriod: FiniteDuration = 10.minutes
/**
* The duration for which we delay triggering when a file is deleted. This is needed because
* many programs implement save as a file move of a temporary file onto the target file.
* Depending on how the move is implemented, this may be detected as a deletion immediately
* followed by a creation. If we trigger immediately on delete, we may, for example, try to
* compile before all of the source files are actually available. The longer this value is set,
* the less likely we are to spuriously trigger a build before all files are available, but
* the longer it will take to trigger a build when the file is actually deleted and not renamed.
*/
final val defaultDeletionQuarantinePeriod: FiniteDuration = 50.milliseconds
/**
* Converts user input to an Action with the following rules:
* 1) on all platforms, new lines exit the watch
* 2) on posix platforms, 'r' or 'R' will trigger a build
* 3) on posix platforms, 's' or 'S' will exit the watch and run the shell command. This is to
* support the case where the user starts sbt in a continuous mode but wants to return to
* the shell without having to restart sbt.
*/
final val defaultInputParser: Parser[Action] = {
def posixOnly(legal: String, action: Action): Parser[Action] =
if (!Util.isWindows) chars(legal) ^^^ action
else Parser.invalid(Seq("Can't use jline for individual character entry on windows."))
val rebuildParser: Parser[Action] = posixOnly(legal = "rR", Trigger)
val shellParser: Parser[Action] = posixOnly(legal = "sS", new Run("shell"))
val cancelParser: Parser[Action] = chars(legal = "\n\r") ^^^ CancelWatch
shellParser | rebuildParser | cancelParser
}
/**
* A function that prints out the current iteration count and gives instructions for exiting
* or triggering the build.
*/
val defaultStartWatch: Int => Option[String] =
((count: Int) => Some(s"$count. ${waitMessage("")}")).label("Watched.defaultStartWatch")
/**
* Default no-op callback.
*/
val defaultOnEnter: () => Unit = () => {}
private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State =
onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination")
private[sbt] val defaultTaskOnTermination: (Action, String, Int, State) => State =
onTerminationImpl("watch", ContinuousExecutePrefix).label("Watched.defaultTaskOnTermination")
/**
* Default handler to transform the state when the watch terminates. When the [[Watched.Action]]
* is [[Reload]], the handler will prepend the original command (prefixed by ~) to the
* [[State.remainingCommands]] and then invoke the [[StateOps.reload]] method. When the
* [[Watched.Action]] is [[HandleError]], the handler returns the result of [[StateOps.fail]].
* When the [[Watched.Action]] is [[Watched.Run]], we add the commands specified by
* [[Watched.Run.commands]] to the stat's remaining commands. Otherwise the original state is
* returned.
*/
private def onTerminationImpl(
watchPrefixes: String*
): (Action, String, Int, State) => State = { (action, command, count, state) =>
val prefix = watchPrefixes.head
val rc = state.remainingCommands
.filterNot(c => watchPrefixes.exists(c.commandLine.trim.startsWith))
action match {
case Run(commands) => state.copy(remainingCommands = commands ++ rc)
case Reload =>
state.copy(remainingCommands = "reload".toExec :: s"$prefix $count $command".toExec :: rc)
case _: HandleError => state.copy(remainingCommands = rc).fail
case _ => state.copy(remainingCommands = rc)
}
}
/**
* A constant function that always returns [[None]]. When
* `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when
* a build is triggered.
*/
final val defaultOnTriggerMessage: (Int, Event[FileAttributes]) => Option[String] =
((_: Int, _: Event[FileAttributes]) => None).label("Watched.defaultOnTriggerMessage")
/**
* The minimum delay between file system polling when a [[PollingWatchService]] is used.
*/
final val defaultPollInterval: FiniteDuration = 500.milliseconds
/**
* A constant function that returns an Option wrapped string that clears the screen when
* written to stdout.
*/
final val clearOnTrigger: Int => Option[String] =
((_: Int) => Some(clearScreen)).label("Watched.clearOnTrigger")
def clearScreen: String = "\u001b[2J\u001b[0;0H" def clearScreen: String = "\u001b[2J\u001b[0;0H"
@deprecated("WatchSource has been replaced by sbt.io.Glob", "1.3.0") @deprecated("WatchSource has been replaced by sbt.io.Glob", "1.3.0")
@ -361,87 +84,6 @@ object Watched {
} }
private type RunCommand = () => State
private type NextAction = () => Watched.Action
private[sbt] type Monitor = FileEventMonitor[FileAttributes]
/**
* Runs a task and then blocks until the task is ready to run again or we no longer wish to
* block execution.
*
* @param task the aggregated task to run with each iteration
* @param onStart function to be invoked before we start polling for events
* @param nextAction function that returns the next state transition [[Watched.Action]].
* @return the exit [[Watched.Action]] that can be used to potentially modify the build state and
* the count of the number of iterations that were run. If
*/
def watch(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watched.Action = {
def safeNextAction(delegate: NextAction): Watched.Action =
try delegate()
catch { case NonFatal(t) => new HandleError(t) }
@tailrec def next(): Watched.Action = safeNextAction(nextAction) match {
// This should never return Ignore due to this condition.
case Ignore => next()
case action => action
}
@tailrec def impl(): Watched.Action = {
task()
safeNextAction(onStart) match {
case Ignore =>
next() match {
case Trigger => impl()
case action => action
}
case Trigger => impl()
case a => a
}
}
try impl()
catch { case NonFatal(t) => new HandleError(t) }
}
private[sbt] object NullLogger extends Logger {
override def trace(t: => Throwable): Unit = {}
override def success(message: => String): Unit = {}
override def log(level: Level.Value, message: => String): Unit = {}
}
/**
* Traverse all of the events and find the one for which we give the highest
* weight. Within the [[Action]] hierarchy:
* [[Custom]] > [[HandleError]] > [[Run]] > [[CancelWatch]] > [[Reload]] > [[Trigger]] > [[Ignore]]
* the first event of each kind is returned so long as there are no higher priority events
* in the collection. For example, if there are multiple events that all return [[Trigger]], then
* the first one is returned. If, on the other hand, one of the events returns [[Reload]],
* then that event "wins" and the [[Reload]] action is returned with the [[Event[FileAttributes]]]
* that triggered it.
*
* @param events the ([[Action]], [[Event[FileAttributes]]]) pairs
* @return the ([[Action]], [[Event[FileAttributes]]]) pair with highest weight if the input events
* are non empty.
*/
@inline
private[sbt] def aggregate(
events: Seq[(Action, Event[FileAttributes])]
): Option[(Action, Event[FileAttributes])] =
if (events.isEmpty) None else Some(events.minBy(_._1))
private implicit class StringToExec(val s: String) extends AnyVal {
def toExec: Exec = Exec(s, None)
}
private[sbt] def withCharBufferedStdIn[R](f: InputStream => R): R =
if (!Util.isWindows) JLine.usingTerminal { terminal =>
terminal.init()
val in = terminal.wrapInIfNeeded(System.in)
try {
f(in)
} finally {
terminal.reset()
}
} else
f(System.in)
private[sbt] val newWatchService: () => WatchService = private[sbt] val newWatchService: () => WatchService =
(() => createWatchService()).label("Watched.newWatchService") (() => createWatchService()).label("Watched.newWatchService")
def createWatchService(pollDelay: FiniteDuration): WatchService = { def createWatchService(pollDelay: FiniteDuration): WatchService = {
@ -479,9 +121,9 @@ object Watched {
((ws: WatchState) => projectOnWatchMessage(projectId)(ws.count).get) ((ws: WatchState) => projectOnWatchMessage(projectId)(ws.count).get)
.label("Watched.projectWatchingMessage") .label("Watched.projectWatchingMessage")
@deprecated("unused", "1.3.0") @deprecated("unused", "1.3.0")
def projectOnWatchMessage(project: String): Int => Option[String] = def projectOnWatchMessage(project: String): Int => Option[String] = { (count: Int) =>
((count: Int) => Some(s"$count. ${waitMessage(s" in project $project")}")) Some(s"$count. ${waitMessage(s" in project $project")}")
.label("Watched.projectOnWatchMessage") }.label("Watched.projectOnWatchMessage")
@deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0") @deprecated("This method is not used and may be removed in a future version of sbt", "1.3.0")
private[this] class AWatched extends Watched private[this] class AWatched extends Watched
@ -522,7 +164,8 @@ object Watched {
@deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0") @deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0")
val defaultWatchingMessage: WatchState => String = val defaultWatchingMessage: WatchState => String =
((ws: WatchState) => defaultStartWatch(ws.count).get).label("Watched.defaultWatchingMessage") ((ws: WatchState) => s"${ws.count}. ${waitMessage("")} ")
.label("Watched.projectWatchingMessage")
@deprecated( @deprecated(
"Use defaultOnTriggerMessage in conjunction with the watchTriggeredMessage key", "Use defaultOnTriggerMessage in conjunction with the watchTriggeredMessage key",
"1.3.0" "1.3.0"

View File

@ -333,21 +333,21 @@ object Defaults extends BuildCommon {
insideCI :== sys.env.contains("BUILD_NUMBER") || insideCI :== sys.env.contains("BUILD_NUMBER") ||
sys.env.contains("CI") || System.getProperty("sbt.ci", "false") == "true", sys.env.contains("CI") || System.getProperty("sbt.ci", "false") == "true",
// watch related settings // watch related settings
pollInterval :== Watched.defaultPollInterval, pollInterval :== Watch.defaultPollInterval,
watchAntiEntropy :== Watched.defaultAntiEntropy, watchAntiEntropy :== Watch.defaultAntiEntropy,
watchAntiEntropyRetentionPeriod :== Watched.defaultAntiEntropyRetentionPeriod, watchAntiEntropyRetentionPeriod :== Watch.defaultAntiEntropyRetentionPeriod,
watchLogLevel :== Level.Info, watchLogLevel :== Level.Info,
watchOnEnter :== Watched.defaultOnEnter, watchOnEnter :== Watch.defaultOnEnter,
watchOnMetaBuildEvent :== Watched.ifChanged(Watched.Reload), watchOnMetaBuildEvent :== Watch.ifChanged(Watch.Reload),
watchOnInputEvent :== Watched.trigger, watchOnInputEvent :== Watch.trigger,
watchOnTriggerEvent :== Watched.trigger, watchOnTriggerEvent :== Watch.trigger,
watchDeletionQuarantinePeriod :== Watched.defaultDeletionQuarantinePeriod, watchDeletionQuarantinePeriod :== Watch.defaultDeletionQuarantinePeriod,
watchService :== Watched.newWatchService, watchService :== Watched.newWatchService,
watchStartMessage :== Watched.defaultStartWatch, watchStartMessage :== Watch.defaultStartWatch,
watchTasks := Continuous.continuousTask.evaluated, watchTasks := Continuous.continuousTask.evaluated,
aggregate in watchTasks :== false, aggregate in watchTasks :== false,
watchTrackMetaBuild :== true, watchTrackMetaBuild :== true,
watchTriggeredMessage :== Watched.defaultOnTriggerMessage, watchTriggeredMessage :== Watch.defaultOnTriggerMessage,
) )
) )

View File

@ -100,19 +100,19 @@ object Keys {
val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration]("Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely.").withRank(BMinusSetting) val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration]("Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely.").withRank(BMinusSetting)
val watchDeletionQuarantinePeriod = settingKey[FiniteDuration]("Period for which deletion events will be quarantined. This is to prevent spurious builds when a file is updated with a rename which manifests as a file deletion followed by a file creation. The higher this value is set, the longer the delay will be between a file deletion and a build trigger but the less likely it is for a spurious trigger.").withRank(DSetting) val watchDeletionQuarantinePeriod = settingKey[FiniteDuration]("Period for which deletion events will be quarantined. This is to prevent spurious builds when a file is updated with a rename which manifests as a file deletion followed by a file creation. The higher this value is set, the longer the delay will be between a file deletion and a build trigger but the less likely it is for a spurious trigger.").withRank(DSetting)
val watchLogLevel = settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.").withRank(DSetting) val watchLogLevel = settingKey[sbt.util.Level.Value]("Transform the default logger in continuous builds.").withRank(DSetting)
val watchInputHandler = settingKey[InputStream => Watched.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 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]("The input stream to read for user input events. This will usually be System.in").withRank(DSetting) val watchInputStream = taskKey[InputStream]("The input stream to read for user input events. This will usually be System.in").withRank(DSetting)
val watchInputParser = settingKey[Parser[Watched.Action]]("A parser of user input that can be used to trigger or exit a continuous build").withRank(DSetting) 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 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 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 watchOnInputEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) val watchOnInputEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive inputs. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting)
val watchOnEvent = settingKey[Continuous.Arguments => Event[FileAttributes] => Watched.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting) val watchOnEvent = settingKey[Continuous.Arguments => Event[FileAttributes] => Watch.Action]("Determines how to handle a file event. The Seq[Glob] contains all of the transitive inputs for the task(s) being run by the continuous build.").withRank(DSetting)
val watchOnMetaBuildEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting) val watchOnMetaBuildEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the meta build triggers.").withRank(DSetting)
val watchOnTermination = settingKey[(Watched.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting) val watchOnTermination = settingKey[(Watch.Action, String, Int, State) => State]("Transforms the state upon completion of a watch. The String argument is the command that was run during the watch. The Int parameter specifies how many times the command was run during the watch.").withRank(DSetting)
val watchOnTrigger = settingKey[Continuous.Arguments => Event[FileAttributes] => Unit]("Callback to invoke when a continuous build triggers. The first parameter is the number of previous watch task invocations. The second parameter is the Event that triggered this build").withRank(DSetting) val watchOnTrigger = settingKey[Continuous.Arguments => Event[FileAttributes] => Unit]("Callback to invoke when a continuous build triggers. The first parameter is the number of previous watch task invocations. The second parameter is the Event that triggered this build").withRank(DSetting)
val watchOnTriggerEvent = settingKey[(Int, Event[FileAttributes]) => Watched.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting) val watchOnTriggerEvent = settingKey[(Int, Event[FileAttributes]) => Watch.Action]("Callback to invoke if an event is triggered in a continuous build by one of the transitive triggers. This is only invoked if watchOnEvent is not explicitly set.").withRank(DSetting)
val watchOnIteration = settingKey[Int => Watched.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting) val watchOnIteration = settingKey[Int => Watch.Action]("Function that is invoked before waiting for file system events or user input events. This is only invoked if watchOnStart is not explicitly set.").withRank(DSetting)
val watchOnStart = settingKey[Continuous.Arguments => () => Watched.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting) val watchOnStart = settingKey[Continuous.Arguments => () => Watch.Action]("Function is invoked before waiting for file system or input events. The returned Action is used to either trigger the build, terminate the watch or wait for events.").withRank(DSetting)
val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting) val watchService = settingKey[() => WatchService]("Service to use to monitor file system changes.").withRank(BMinusSetting).withRank(DSetting)
val watchStartMessage = settingKey[Int => Option[String]]("The message to show when triggered execution waits for sources to change. The parameter is the current watch iteration count.").withRank(DSetting) val watchStartMessage = settingKey[Int => Option[String]]("The message to show when triggered execution waits for sources to change. The parameter is the current watch iteration count.").withRank(DSetting)
// The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task. // The watchTasks key should really be named watch, but that is already taken by the deprecated watch key. I'd be surprised if there are any plugins that use it so I think we should consider breaking binary compatibility to rename this task.

View File

@ -0,0 +1,373 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.InputStream
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.internal.FileAttributes
import sbt.internal.LabeledFunctions._
import sbt.internal.util.{ JLine, Util }
import sbt.internal.util.complete.Parser
import sbt.internal.util.complete.Parser._
import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update }
import sbt.util.{ Level, Logger }
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.control.NonFatal
object Watch {
/**
* This trait is used to control the state of [[Watch.apply]]. The [[Watch.Trigger]] action
* indicates that [[Watch.apply]] should re-run the input task. The [[Watch.CancelWatch]]
* actions indicate that [[Watch.apply]] should exit and return the [[Watch.CancelWatch]]
* instance that caused the function to exit. The [[Watch.Ignore]] action is used to indicate
* that the method should keep polling for new actions.
*/
sealed trait Action
/**
* Provides a default `Ordering` for actions. Lower values correspond to higher priority actions.
* [[CancelWatch]] is higher priority than [[ContinueWatch]].
*/
object Action {
implicit object ordering extends Ordering[Action] {
override def compare(left: Action, right: Action): Int = (left, right) match {
case (a: ContinueWatch, b: ContinueWatch) => ContinueWatch.ordering.compare(a, b)
case (_: ContinueWatch, _: CancelWatch) => 1
case (a: CancelWatch, b: CancelWatch) => CancelWatch.ordering.compare(a, b)
case (_: CancelWatch, _: ContinueWatch) => -1
}
}
}
/**
* Action that indicates that the watch should stop.
*/
sealed trait CancelWatch extends Action
/**
* Action that does not terminate the watch but might trigger a build.
*/
sealed trait ContinueWatch extends Action
/**
* Provides a default `Ordering` for classes extending [[ContinueWatch]]. [[Trigger]] is higher
* priority than [[Ignore]].
*/
object ContinueWatch {
/**
* A default `Ordering` for [[ContinueWatch]]. [[Trigger]] is higher priority than [[Ignore]].
*/
implicit object ordering extends Ordering[ContinueWatch] {
override def compare(left: ContinueWatch, right: ContinueWatch): Int = left match {
case Ignore => if (right == Ignore) 0 else 1
case Trigger => if (right == Trigger) 0 else -1
}
}
}
/**
* Action that indicates that the watch should stop.
*/
case object CancelWatch extends CancelWatch {
/**
* A default `Ordering` for [[ContinueWatch]]. The priority of each type of [[CancelWatch]]
* is reflected by the ordering of the case statements in the [[ordering.compare]] method,
* e.g. [[Custom]] is higher priority than [[HandleError]].
*/
implicit object ordering extends Ordering[CancelWatch] {
override def compare(left: CancelWatch, right: CancelWatch): Int = left match {
// Note that a negative return value means the left CancelWatch is preferred to the right
// CancelWatch while the inverse is true for a positive return value. This logic could
// likely be simplified, but the pattern matching approach makes it very clear what happens
// for each type of Action.
case _: Custom =>
right match {
case _: Custom => 0
case _ => -1
}
case _: HandleError =>
right match {
case _: Custom => 1
case _: HandleError => 0
case _ => -1
}
case _: Run =>
right match {
case _: Run => 0
case CancelWatch | Reload => -1
case _ => 1
}
case CancelWatch =>
right match {
case CancelWatch => 0
case Reload => -1
case _ => 1
}
case Reload => if (right == Reload) 0 else 1
}
}
}
/**
* Action that indicates that an error has occurred. The watch will be terminated when this action
* is produced.
*/
final class HandleError(val throwable: Throwable) extends CancelWatch {
override def equals(o: Any): Boolean = o match {
case that: HandleError => this.throwable == that.throwable
case _ => false
}
override def hashCode: Int = throwable.hashCode
override def toString: String = s"HandleError($throwable)"
}
/**
* Action that indicates that the watch should continue as though nothing happened. This may be
* because, for example, no user input was yet available.
*/
case object Ignore extends ContinueWatch
/**
* Action that indicates that the watch should pause while the build is reloaded. This is used to
* automatically reload the project when the build files (e.g. build.sbt) are changed.
*/
case object Reload extends CancelWatch
/**
* Action that indicates that we should exit and run the provided command.
* @param commands the commands to run after we exit the watch
*/
final class Run(val commands: String*) extends CancelWatch
// For now leave this private in case this isn't the best unapply type signature since it can't
// be evolved in a binary compatible way.
private object Run {
def unapply(r: Run): Option[List[Exec]] = Some(r.commands.toList.map(Exec(_, None)))
}
/**
* Action that indicates that the watch process should re-run the command.
*/
case object Trigger extends ContinueWatch
/**
* A user defined Action. It is not sealed so that the user can create custom instances. If
* the onStart or nextAction function passed into [[Watch.apply]] returns [[Watch.Custom]], then
* the watch will terminate.
*/
trait Custom extends CancelWatch
private type NextAction = () => Watch.Action
/**
* Runs a task and then blocks until the task is ready to run again or we no longer wish to
* block execution.
*
* @param task the aggregated task to run with each iteration
* @param onStart function to be invoked before we start polling for events
* @param nextAction function that returns the next state transition [[Watch.Action]].
* @return the exit [[Watch.Action]] that can be used to potentially modify the build state and
* the count of the number of iterations that were run. If
*/
def apply(task: () => Unit, onStart: NextAction, nextAction: NextAction): Watch.Action = {
def safeNextAction(delegate: NextAction): Watch.Action =
try delegate()
catch { case NonFatal(t) => new HandleError(t) }
@tailrec def next(): Watch.Action = safeNextAction(nextAction) match {
// This should never return Ignore due to this condition.
case Ignore => next()
case action => action
}
@tailrec def impl(): Watch.Action = {
task()
safeNextAction(onStart) match {
case Ignore =>
next() match {
case Trigger => impl()
case action => action
}
case Trigger => impl()
case a => a
}
}
try impl()
catch { case NonFatal(t) => new HandleError(t) }
}
private[sbt] object NullLogger extends Logger {
override def trace(t: => Throwable): Unit = {}
override def success(message: => String): Unit = {}
override def log(level: Level.Value, message: => String): Unit = {}
}
/**
* Traverse all of the events and find the one for which we give the highest
* weight. Within the [[Action]] hierarchy:
* [[Custom]] > [[HandleError]] > [[CancelWatch]] > [[Reload]] > [[Trigger]] > [[Ignore]]
* the first event of each kind is returned so long as there are no higher priority events
* in the collection. For example, if there are multiple events that all return [[Trigger]], then
* the first one is returned. If, on the other hand, one of the events returns [[Reload]],
* then that event "wins" and the [[Reload]] action is returned with the [[Event[FileAttributes]]] that triggered it.
*
* @param events the ([[Action]], [[Event[FileAttributes]]]) pairs
* @return the ([[Action]], [[Event[FileAttributes]]]) pair with highest weight if the input events
* are non empty.
*/
@inline
private[sbt] def aggregate(
events: Seq[(Action, Event[FileAttributes])]
): Option[(Action, Event[FileAttributes])] =
if (events.isEmpty) None else Some(events.minBy(_._1))
private implicit class StringToExec(val s: String) extends AnyVal {
def toExec: Exec = Exec(s, None)
}
private[sbt] def withCharBufferedStdIn[R](f: InputStream => R): R =
if (!Util.isWindows) JLine.usingTerminal { terminal =>
terminal.init()
val in = terminal.wrapInIfNeeded(System.in)
try {
f(in)
} finally {
terminal.reset()
}
} else
f(System.in)
/**
* A constant function that returns [[Trigger]].
*/
final val trigger: (Int, Event[FileAttributes]) => Watch.Action = {
(_: Int, _: Event[FileAttributes]) =>
Trigger
}.label("Watched.trigger")
def ifChanged(action: Action): (Int, Event[FileAttributes]) => Watch.Action =
(_: Int, event: Event[FileAttributes]) =>
event match {
case Update(prev, cur, _) if prev.value != cur.value => action
case _: Creation[_] | _: Deletion[_] => action
case _ => Ignore
}
/**
* The minimum delay between build triggers for the same file. If the file is detected
* to have changed within this period from the last build trigger, the event will be discarded.
*/
final val defaultAntiEntropy: FiniteDuration = 500.milliseconds
/**
* The duration in wall clock time for which a FileEventMonitor will retain anti-entropy
* events for files. This is an implementation detail of the FileEventMonitor. It should
* hopefully not need to be set by the users. It is needed because when a task takes a long time
* to run, it is possible that events will be detected for the file that triggered the build that
* occur within the anti-entropy period. We still allow it to be configured to limit the memory
* usage of the FileEventMonitor (but this is somewhat unlikely to be a problem).
*/
final val defaultAntiEntropyRetentionPeriod: FiniteDuration = 10.minutes
/**
* The duration for which we delay triggering when a file is deleted. This is needed because
* many programs implement save as a file move of a temporary file onto the target file.
* Depending on how the move is implemented, this may be detected as a deletion immediately
* followed by a creation. If we trigger immediately on delete, we may, for example, try to
* compile before all of the source files are actually available. The longer this value is set,
* the less likely we are to spuriously trigger a build before all files are available, but
* the longer it will take to trigger a build when the file is actually deleted and not renamed.
*/
final val defaultDeletionQuarantinePeriod: FiniteDuration = 50.milliseconds
/**
* Converts user input to an Action with the following rules:
* 1) on all platforms, new lines exit the watch
* 2) on posix platforms, 'r' or 'R' will trigger a build
* 3) on posix platforms, 's' or 'S' will exit the watch and run the shell command. This is to
* support the case where the user starts sbt in a continuous mode but wants to return to
* the shell without having to restart sbt.
*/
final val defaultInputParser: Parser[Action] = {
def posixOnly(legal: String, action: Action): Parser[Action] =
if (!Util.isWindows) chars(legal) ^^^ action
else Parser.invalid(Seq("Can't use jline for individual character entry on windows."))
val rebuildParser: Parser[Action] = posixOnly(legal = "rR", Trigger)
val shellParser: Parser[Action] = posixOnly(legal = "sS", new Run("shell"))
val cancelParser: Parser[Action] = chars(legal = "\n\r") ^^^ CancelWatch
shellParser | rebuildParser | cancelParser
}
private[this] val reRun =
if (Util.isWindows) "" else ", 'r' to re-run the command or 's' to return to the shell"
private[sbt] def waitMessage(project: String): String =
s"Waiting for source changes$project... (press enter to interrupt$reRun)"
/**
* A function that prints out the current iteration count and gives instructions for exiting
* or triggering the build.
*/
val defaultStartWatch: Int => Option[String] =
((count: Int) => Some(s"$count. ${waitMessage("")}")).label("Watched.defaultStartWatch")
/**
* Default no-op callback.
*/
val defaultOnEnter: () => Unit = () => {}
private[sbt] val defaultCommandOnTermination: (Action, String, Int, State) => State =
onTerminationImpl(ContinuousExecutePrefix).label("Watched.defaultCommandOnTermination")
private[sbt] val defaultTaskOnTermination: (Action, String, Int, State) => State =
onTerminationImpl("watch", ContinuousExecutePrefix).label("Watched.defaultTaskOnTermination")
/**
* Default handler to transform the state when the watch terminates. When the [[Watch.Action]]
* is [[Reload]], the handler will prepend the original command (prefixed by ~) to the
* [[State.remainingCommands]] and then invoke the [[StateOps.reload]] method. When the
* [[Watch.Action]] is [[HandleError]], the handler returns the result of [[StateOps.fail]].
* When the [[Watch.Action]] is [[Watch.Run]], we add the commands specified by
* [[Watch.Run.commands]] to the stat's remaining commands. Otherwise the original state is
* returned.
*/
private def onTerminationImpl(
watchPrefixes: String*
): (Action, String, Int, State) => State = { (action, command, count, state) =>
val prefix = watchPrefixes.head
val rc = state.remainingCommands
.filterNot(c => watchPrefixes.exists(c.commandLine.trim.startsWith))
action match {
case Run(commands) => state.copy(remainingCommands = commands ++ rc)
case Reload =>
state.copy(remainingCommands = "reload".toExec :: s"$prefix $count $command".toExec :: rc)
case _: HandleError => state.copy(remainingCommands = rc).fail
case _ => state.copy(remainingCommands = rc)
}
}
/**
* A constant function that always returns `None`. When
* `Keys.watchTriggeredMessage := Watched.defaultOnTriggerMessage`, then nothing is logged when
* a build is triggered.
*/
final val defaultOnTriggerMessage: (Int, Event[FileAttributes]) => Option[String] =
((_: Int, _: Event[FileAttributes]) => None).label("Watched.defaultOnTriggerMessage")
/**
* The minimum delay between file system polling when a `PollingWatchService` is used.
*/
final val defaultPollInterval: FiniteDuration = 500.milliseconds
/**
* A constant function that returns an Option wrapped string that clears the screen when
* written to stdout.
*/
final val clearOnTrigger: Int => Option[String] =
((_: Int) => Some(Watched.clearScreen)).label("Watched.clearOnTrigger")
}

View File

@ -9,38 +9,28 @@ package sbt
package internal package internal
import java.io.IOException import java.io.IOException
import java.net.Socket
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic._ import java.util.concurrent.atomic._
import scala.collection.mutable.ListBuffer import sbt.BasicKeys._
import scala.annotation.tailrec import sbt.Watch.NullLogger
import BasicKeys.{ import sbt.internal.langserver.{ LogMessageParams, MessageType }
autoStartServer, import sbt.internal.server._
fullServerHandlers, import sbt.internal.util.codec.JValueFormats
logLevel, import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent }
serverAuthentication, import sbt.io.syntax._
serverConnectionType, import sbt.io.{ Hash, IO }
serverHost, import sbt.protocol.{ EventMessage, ExecStatusEvent }
serverLogLevel, import sbt.util.{ Level, LogExchange, Logger }
serverPort
}
import java.net.Socket
import sbt.Watched.NullLogger
import sjsonnew.JsonFormat import sjsonnew.JsonFormat
import sjsonnew.shaded.scalajson.ast.unsafe._ import sjsonnew.shaded.scalajson.ast.unsafe._
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{ Failure, Success, Try } import scala.util.{ Failure, Success, Try }
import sbt.io.syntax._
import sbt.io.{ Hash, IO }
import sbt.internal.server._
import sbt.internal.langserver.{ LogMessageParams, MessageType }
import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent }
import sbt.internal.util.codec.JValueFormats
import sbt.protocol.{ EventMessage, ExecStatusEvent }
import sbt.util.{ Level, LogExchange, Logger }
/** /**
* The command exchange merges multiple command channels (e.g. network and console), * The command exchange merges multiple command channels (e.g. network and console),

View File

@ -20,13 +20,13 @@ import sbt.BasicCommandStrings.{
import sbt.BasicCommands.otherCommandParser import sbt.BasicCommands.otherCommandParser
import sbt.Def._ import sbt.Def._
import sbt.Scope.Global import sbt.Scope.Global
import sbt.Watched.Monitor
import sbt.internal.FileManagement.FileTreeRepositoryOps import sbt.internal.FileManagement.FileTreeRepositoryOps
import sbt.internal.LabeledFunctions._
import sbt.internal.io.WatchState import sbt.internal.io.WatchState
import sbt.internal.util.Types.const import sbt.internal.util.Types.const
import sbt.internal.util.complete.Parser._ import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers } import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, AttributeMap } import sbt.internal.util.{ AttributeKey, AttributeMap, Util }
import sbt.io._ import sbt.io._
import sbt.util.{ Level, _ } import sbt.util.{ Level, _ }
@ -92,17 +92,17 @@ object Continuous extends DeprecatedContinuous {
} }
/** /**
* Create a function from InputStream => [[Watched.Action]] from a [[Parser]]. This is intended * Create a function from InputStream => [[Watch.Action]] from a [[Parser]]. This is intended
* to be used to set the watchInputHandler setting for a task. * to be used to set the watchInputHandler setting for a task.
* @param parser the parser * @param parser the parser
* @return the function * @return the function
*/ */
def defaultInputHandler(parser: Parser[Watched.Action]): InputStream => Watched.Action = { def defaultInputHandler(parser: Parser[Watch.Action]): InputStream => Watch.Action = {
val builder = new StringBuilder val builder = new StringBuilder
val any = matched(Parsers.any.*) val any = matched(Parsers.any.*)
val fullParser = any ~> parser ~ any val fullParser = any ~> parser ~ any
inputStream => ((inputStream: InputStream) => parse(inputStream, builder, fullParser))
parse(inputStream, builder, fullParser) .label("Continuous.defaultInputHandler")
} }
/** /**
@ -254,7 +254,7 @@ object Continuous extends DeprecatedContinuous {
command: String, command: String,
count: Int, count: Int,
isCommand: Boolean isCommand: Boolean
): State = Watched.withCharBufferedStdIn { in => ): State = Watch.withCharBufferedStdIn { in =>
val duped = new DupedInputStream(in) val duped = new DupedInputStream(in)
setup(state.put(DupedSystemIn, duped), command) { (s, commands, valid, invalid) => setup(state.put(DupedSystemIn, duped), command) { (s, commands, valid, invalid) =>
implicit val extracted: Extracted = Project.extract(s) implicit val extracted: Extracted = Project.extract(s)
@ -276,7 +276,7 @@ object Continuous extends DeprecatedContinuous {
// or Watched.Reload. The task defined above will be run at least once. It will be run // or Watched.Reload. The task defined above will be run at least once. It will be run
// additional times whenever the state transition callbacks return Watched.Trigger. // additional times whenever the state transition callbacks return Watched.Trigger.
try { try {
val terminationAction = Watched.watch(task, callbacks.onStart, callbacks.nextEvent) val terminationAction = Watch(task, callbacks.onStart, callbacks.nextEvent)
callbacks.onTermination(terminationAction, command, currentCount.get(), state) callbacks.onTermination(terminationAction, command, currentCount.get(), state)
} finally callbacks.onExit() } finally callbacks.onExit()
} else { } else {
@ -315,11 +315,11 @@ object Continuous extends DeprecatedContinuous {
} }
private class Callbacks( private class Callbacks(
val nextEvent: () => Watched.Action, val nextEvent: () => Watch.Action,
val onEnter: () => Unit, val onEnter: () => Unit,
val onExit: () => Unit, val onExit: () => Unit,
val onStart: () => Watched.Action, val onStart: () => Watch.Action,
val onTermination: (Watched.Action, String, Int, State) => State val onTermination: (Watch.Action, String, Int, State) => State
) )
/** /**
@ -329,19 +329,19 @@ object Continuous extends DeprecatedContinuous {
* To monitor all of the inputs and triggers, it creates a [[FileEventMonitor]] for each task * To monitor all of the inputs and triggers, it creates a [[FileEventMonitor]] for each task
* and then aggregates each of the individual [[FileEventMonitor]] instances into an aggregated * and then aggregates each of the individual [[FileEventMonitor]] instances into an aggregated
* instance. It aggregates all of the event callbacks into a single callback that delegates * instance. It aggregates all of the event callbacks into a single callback that delegates
* to each of the individual callbacks. For the callbacks that return a [[Watched.Action]], * to each of the individual callbacks. For the callbacks that return a [[Watch.Action]],
* the aggregated callback will select the minimum [[Watched.Action]] returned where the ordering * the aggregated callback will select the minimum [[Watch.Action]] returned where the ordering
* is such that the highest priority [[Watched.Action]] have the lowest values. Finally, to * is such that the highest priority [[Watch.Action]] have the lowest values. Finally, to
* handle user input, we read from the provided input stream and buffer the result. Each * handle user input, we read from the provided input stream and buffer the result. Each
* task's input parser is then applied to the buffered result and, again, we return the mimimum * task's input parser is then applied to the buffered result and, again, we return the mimimum
* [[Watched.Action]] returned by the parsers (when the parsers fail, they just return * [[Watch.Action]] returned by the parsers (when the parsers fail, they just return
* [[Watched.Ignore]], which is the lowest priority [[Watched.Action]]. * [[Watch.Ignore]], which is the lowest priority [[Watch.Action]].
* *
* @param configs the [[Config]] instances * @param configs the [[Config]] instances
* @param rawLogger the default sbt logger instance * @param rawLogger the default sbt logger instance
* @param state the current state * @param state the current state
* @param extracted the [[Extracted]] instance for the current build * @param extracted the [[Extracted]] instance for the current build
* @return the [[Callbacks]] to pass into [[Watched.watch]] * @return the [[Callbacks]] to pass into [[Watch.apply]]
*/ */
private def aggregate( private def aggregate(
configs: Seq[Config], configs: Seq[Config],
@ -355,11 +355,11 @@ object Continuous extends DeprecatedContinuous {
): Callbacks = { ): Callbacks = {
val logger = setLevel(rawLogger, configs.map(_.watchSettings.logLevel).min, state) val logger = setLevel(rawLogger, configs.map(_.watchSettings.logLevel).min, state)
val onEnter = () => configs.foreach(_.watchSettings.onEnter()) val onEnter = () => configs.foreach(_.watchSettings.onEnter())
val onStart: () => Watched.Action = getOnStart(configs, logger, count) val onStart: () => Watch.Action = getOnStart(configs, logger, count)
val nextInputEvent: () => Watched.Action = parseInputEvents(configs, state, inputStream, logger) val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger)
val (nextFileEvent, cleanupFileMonitor): (() => Watched.Action, () => Unit) = val (nextFileEvent, cleanupFileMonitor): (() => Watch.Action, () => Unit) =
getFileEvents(configs, logger, state, count) getFileEvents(configs, logger, state, count)
val nextEvent: () => Watched.Action = val nextEvent: () => Watch.Action =
combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger) combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger)
val onExit = () => { val onExit = () => {
cleanupFileMonitor() cleanupFileMonitor()
@ -372,7 +372,7 @@ object Continuous extends DeprecatedContinuous {
private def getOnTermination( private def getOnTermination(
configs: Seq[Config], configs: Seq[Config],
isCommand: Boolean isCommand: Boolean
): (Watched.Action, String, Int, State) => State = { ): (Watch.Action, String, Int, State) => State = {
configs.flatMap(_.watchSettings.onTermination).distinct match { configs.flatMap(_.watchSettings.onTermination).distinct match {
case Seq(head, tail @ _*) => case Seq(head, tail @ _*) =>
tail.foldLeft(head) { tail.foldLeft(head) {
@ -381,7 +381,7 @@ object Continuous extends DeprecatedContinuous {
configOnTermination(action, cmd, count, onTermination(action, cmd, count, state)) configOnTermination(action, cmd, count, onTermination(action, cmd, count, state))
} }
case _ => case _ =>
if (isCommand) Watched.defaultCommandOnTermination else Watched.defaultTaskOnTermination if (isCommand) Watch.defaultCommandOnTermination else Watch.defaultTaskOnTermination
} }
} }
@ -389,7 +389,7 @@ object Continuous extends DeprecatedContinuous {
configs: Seq[Config], configs: Seq[Config],
logger: Logger, logger: Logger,
count: AtomicInteger count: AtomicInteger
): () => Watched.Action = { ): () => Watch.Action = {
val f = configs.map { params => val f = configs.map { params =>
val ws = params.watchSettings val ws = params.watchSettings
ws.onStart.map(_.apply(params.arguments(logger))).getOrElse { () => ws.onStart.map(_.apply(params.arguments(logger))).getOrElse { () =>
@ -398,10 +398,10 @@ object Continuous extends DeprecatedContinuous {
ws.startMessage match { ws.startMessage match {
case Some(Left(sm)) => logger.info(sm(params.watchState(count.get()))) case Some(Left(sm)) => logger.info(sm(params.watchState(count.get())))
case Some(Right(sm)) => sm(count.get()).foreach(logger.info(_)) case Some(Right(sm)) => sm(count.get()).foreach(logger.info(_))
case None => Watched.defaultStartWatch(count.get()).foreach(logger.info(_)) case None => Watch.defaultStartWatch(count.get()).foreach(logger.info(_))
} }
} }
Watched.Ignore Watch.Ignore
} }
} }
} }
@ -409,7 +409,7 @@ object Continuous extends DeprecatedContinuous {
{ {
val res = f.view.map(_()).min val res = f.view.map(_()).min
// Print the default watch message if there are multiple tasks // Print the default watch message if there are multiple tasks
if (configs.size > 1) Watched.defaultStartWatch(count.get()).foreach(logger.info(_)) if (configs.size > 1) Watch.defaultStartWatch(count.get()).foreach(logger.info(_))
res res
} }
} }
@ -418,7 +418,7 @@ object Continuous extends DeprecatedContinuous {
logger: Logger, logger: Logger,
state: State, state: State,
count: AtomicInteger, count: AtomicInteger,
)(implicit extracted: Extracted): (() => Watched.Action, () => Unit) = { )(implicit extracted: Extracted): (() => Watch.Action, () => Unit) = {
val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild) val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild)
val buildGlobs = val buildGlobs =
if (trackMetaBuild) extracted.getOpt(Keys.fileInputs in Keys.settingsData).getOrElse(Nil) if (trackMetaBuild) extracted.getOpt(Keys.fileInputs in Keys.settingsData).getOrElse(Nil)
@ -430,7 +430,7 @@ object Continuous extends DeprecatedContinuous {
* motivation is to allow the user to specify this callback via setting so that, for example, * motivation is to allow the user to specify this callback via setting so that, for example,
* they can clear the screen when the build triggers. * they can clear the screen when the build triggers.
*/ */
val onTrigger: Event => Watched.Action = { val onTrigger: Event => Watch.Action = {
val f: Seq[Event => Unit] = configs.map { params => val f: Seq[Event => Unit] = configs.map { params =>
val ws = params.watchSettings val ws = params.watchSettings
ws.onTrigger ws.onTrigger
@ -449,38 +449,39 @@ object Continuous extends DeprecatedContinuous {
} }
event: Event => event: Event =>
f.view.foreach(_.apply(event)) f.view.foreach(_.apply(event))
Watched.Trigger Watch.Trigger
} }
val onEvent: Event => (Event, Watched.Action) = { val defaultTrigger = if (Util.isWindows) Watch.ifChanged(Watch.Trigger) else Watch.trigger
val onEvent: Event => (Event, Watch.Action) = {
val f = configs.map { params => val f = configs.map { params =>
val ws = params.watchSettings val ws = params.watchSettings
val oe = ws.onEvent val oe = ws.onEvent
.map(_.apply(params.arguments(logger))) .map(_.apply(params.arguments(logger)))
.getOrElse { .getOrElse {
val onInputEvent = ws.onInputEvent.getOrElse(Watched.trigger) val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger)
val onTriggerEvent = ws.onTriggerEvent.getOrElse(Watched.trigger) val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger)
val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watched.ifChanged(Watched.Reload)) val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload))
val inputFilter = params.inputs.toEntryFilter val inputFilter = params.inputs.toEntryFilter
val triggerFilter = params.triggers.toEntryFilter val triggerFilter = params.triggers.toEntryFilter
event: Event => event: Event =>
val c = count.get() val c = count.get()
Seq[Watched.Action]( Seq[Watch.Action](
if (inputFilter(event.entry)) onInputEvent(c, event) else Watched.Ignore, if (inputFilter(event.entry)) onInputEvent(c, event) else Watch.Ignore,
if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watched.Ignore, if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watch.Ignore,
if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watched.Ignore if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watch.Ignore
).min ).min
} }
event: Event => event: Event =>
event -> (oe(event) match { event -> (oe(event) match {
case Watched.Trigger => onTrigger(event) case Watch.Trigger => onTrigger(event)
case a => a case a => a
}) })
} }
event: Event => event: Event =>
f.view.map(_.apply(event)).minBy(_._2) f.view.map(_.apply(event)).minBy(_._2)
} }
val monitor: Monitor = new FileEventMonitor[FileAttributes] { val monitor: FileEventMonitor[FileAttributes] = new FileEventMonitor[FileAttributes] {
private def setup( private def setup(
monitor: FileEventMonitor[FileAttributes], monitor: FileEventMonitor[FileAttributes],
globs: Seq[Glob] globs: Seq[Glob]
@ -527,11 +528,11 @@ object Continuous extends DeprecatedContinuous {
) )
(() => { (() => {
val actions = antiEntropyMonitor.poll(2.milliseconds).map(onEvent) val actions = antiEntropyMonitor.poll(2.milliseconds).map(onEvent)
if (actions.exists(_._2 != Watched.Ignore)) { if (actions.exists(_._2 != Watch.Ignore)) {
val min = actions.minBy(_._2) val min = actions.minBy(_._2)
logger.debug(s"Received file event actions: ${actions.mkString(", ")}. Returning: $min") logger.debug(s"Received file event actions: ${actions.mkString(", ")}. Returning: $min")
min._2 min._2
} else Watched.Ignore } else Watch.Ignore
}, () => monitor.close()) }, () => monitor.close())
} }
@ -539,16 +540,16 @@ object Continuous extends DeprecatedContinuous {
* Each task has its own input parser that can be used to modify the watch based on the input * Each task has its own input parser that can be used to modify the watch based on the input
* read from System.in as well as a custom task-specific input stream that can be used as * read from System.in as well as a custom task-specific input stream that can be used as
* an alternative source of control. In this method, we create two functions for each task, * an alternative source of control. In this method, we create two functions for each task,
* one from `String => Seq[Watched.Action]` and another from `() => Seq[Watched.Action]`. * one from `String => Seq[Watch.Action]` and another from `() => Seq[Watch.Action]`.
* Each of these functions is invoked to determine the next state transformation for the watch. * Each of these functions is invoked to determine the next state transformation for the watch.
* The first function is a task specific copy of System.in. For each task we keep a mutable * The first function is a task specific copy of System.in. For each task we keep a mutable
* buffer of the characters previously seen from System.in. Every time we receive new characters * buffer of the characters previously seen from System.in. Every time we receive new characters
* we update the buffer and then try to parse a Watched.Action for each task. Any trailing * we update the buffer and then try to parse a Watch.Action for each task. Any trailing
* characters are captured and can be used for the next trigger. Because each task has a local * characters are captured and can be used for the next trigger. Because each task has a local
* copy of the buffer, we do not have to worry about one task breaking parsing of another. We * copy of the buffer, we do not have to worry about one task breaking parsing of another. We
* also provide an alternative per task InputStream that is read in a similar way except that * also provide an alternative per task InputStream that is read in a similar way except that
* we don't need to copy the custom InputStream which allows the function to be * we don't need to copy the custom InputStream which allows the function to be
* `() => Seq[Watched.Action]` which avoids actually exposing the InputStream anywhere. * `() => Seq[Watch.Action]` which avoids actually exposing the InputStream anywhere.
*/ */
private def parseInputEvents( private def parseInputEvents(
configs: Seq[Config], configs: Seq[Config],
@ -557,15 +558,15 @@ object Continuous extends DeprecatedContinuous {
logger: Logger logger: Logger
)( )(
implicit extracted: Extracted implicit extracted: Extracted
): () => Watched.Action = { ): () => Watch.Action = {
/* /*
* This parses the buffer until all possible actions are extracted. By draining the input * This parses the buffer until all possible actions are extracted. By draining the input
* to a state where it does not parse an action, we can wait until we receive new input * to a state where it does not parse an action, we can wait until we receive new input
* to attempt to parse again. * to attempt to parse again.
*/ */
type ActionParser = String => Watched.Action type ActionParser = String => Watch.Action
// Transform the Config.watchSettings.inputParser instances to functions of type // Transform the Config.watchSettings.inputParser instances to functions of type
// String => Watched.Action. The String that is provided will contain any characters that // String => Watch.Action. The String that is provided will contain any characters that
// have been read from stdin. If there are any characters available, then it calls the // have been read from stdin. If there are any characters available, then it calls the
// parse method with the InputStream set to a ByteArrayInputStream that wraps the input // parse method with the InputStream set to a ByteArrayInputStream that wraps the input
// string. The parse method then appends those bytes to a mutable buffer and attempts to // string. The parse method then appends those bytes to a mutable buffer and attempts to
@ -581,7 +582,7 @@ object Continuous extends DeprecatedContinuous {
val systemInBuilder = new StringBuilder val systemInBuilder = new StringBuilder
def inputStream(string: String): InputStream = new ByteArrayInputStream(string.getBytes) def inputStream(string: String): InputStream = new ByteArrayInputStream(string.getBytes)
// This string is provided in the closure below by reading from System.in // This string is provided in the closure below by reading from System.in
val default: String => Watched.Action = val default: String => Watch.Action =
string => parse(inputStream(string), systemInBuilder, parser) string => parse(inputStream(string), systemInBuilder, parser)
val alternative = c.watchSettings.inputStream val alternative = c.watchSettings.inputStream
.map { inputStreamKey => .map { inputStreamKey =>
@ -590,7 +591,7 @@ object Continuous extends DeprecatedContinuous {
() => () =>
handler(is) handler(is)
} }
.getOrElse(() => Watched.Ignore) .getOrElse(() => Watch.Ignore)
(string: String) => (string: String) =>
(default(string) :: alternative() :: Nil).min (default(string) :: alternative() :: Nil).min
} }
@ -599,32 +600,32 @@ object Continuous extends DeprecatedContinuous {
val stringBuilder = new StringBuilder val stringBuilder = new StringBuilder
while (inputStream.available > 0) stringBuilder += inputStream.read().toChar while (inputStream.available > 0) stringBuilder += inputStream.read().toChar
val newBytes = stringBuilder.toString val newBytes = stringBuilder.toString
val parse: ActionParser => Watched.Action = parser => parser(newBytes) val parse: ActionParser => Watch.Action = parser => parser(newBytes)
val allEvents = inputHandlers.map(parse).filterNot(_ == Watched.Ignore) val allEvents = inputHandlers.map(parse).filterNot(_ == Watch.Ignore)
if (allEvents.exists(_ != Watched.Ignore)) { if (allEvents.exists(_ != Watch.Ignore)) {
val res = allEvents.min val res = allEvents.min
logger.debug(s"Received input events: ${allEvents mkString ","}. Taking $res") logger.debug(s"Received input events: ${allEvents mkString ","}. Taking $res")
res res
} else Watched.Ignore } else Watch.Ignore
} }
} }
private def combineInputAndFileEvents( private def combineInputAndFileEvents(
nextInputEvent: () => Watched.Action, nextInputEvent: () => Watch.Action,
nextFileEvent: () => Watched.Action, nextFileEvent: () => Watch.Action,
logger: Logger logger: Logger
): () => Watched.Action = () => { ): () => Watch.Action = () => {
val Seq(inputEvent: Watched.Action, fileEvent: Watched.Action) = val Seq(inputEvent: Watch.Action, fileEvent: Watch.Action) =
Seq(nextInputEvent, nextFileEvent).par.map(_.apply()).toIndexedSeq Seq(nextInputEvent, nextFileEvent).par.map(_.apply()).toIndexedSeq
val min: Watched.Action = Seq[Watched.Action](inputEvent, fileEvent).min val min: Watch.Action = Seq[Watch.Action](inputEvent, fileEvent).min
lazy val inputMessage = lazy val inputMessage =
s"Received input event: $inputEvent." + s"Received input event: $inputEvent." +
(if (inputEvent != min) s" Dropping in favor of file event: $min" else "") (if (inputEvent != min) s" Dropping in favor of file event: $min" else "")
lazy val fileMessage = lazy val fileMessage =
s"Received file event: $fileEvent." + s"Received file event: $fileEvent." +
(if (fileEvent != min) s" Dropping in favor of input event: $min" else "") (if (fileEvent != min) s" Dropping in favor of input event: $min" else "")
if (inputEvent != Watched.Ignore) logger.debug(inputMessage) if (inputEvent != Watch.Ignore) logger.debug(inputMessage)
if (fileEvent != Watched.Ignore) logger.debug(fileMessage) if (fileEvent != Watch.Ignore) logger.debug(fileMessage)
min min
} }
@ -632,8 +633,8 @@ object Continuous extends DeprecatedContinuous {
private final def parse( private final def parse(
is: InputStream, is: InputStream,
builder: StringBuilder, builder: StringBuilder,
parser: Parser[(Watched.Action, String)] parser: Parser[(Watch.Action, String)]
): Watched.Action = { ): Watch.Action = {
if (is.available > 0) builder += is.read().toChar if (is.available > 0) builder += is.read().toChar
Parser.parse(builder.toString, parser) match { Parser.parse(builder.toString, parser) match {
case Right((action, rest)) => case Right((action, rest)) =>
@ -641,7 +642,7 @@ object Continuous extends DeprecatedContinuous {
builder ++= rest builder ++= rest
action action
case _ if is.available > 0 => parse(is, builder, parser) case _ if is.available > 0 => parse(is, builder, parser)
case _ => Watched.Ignore case _ => Watch.Ignore
} }
} }
@ -672,14 +673,14 @@ object Continuous extends DeprecatedContinuous {
} }
} }
private type WatchOnEvent = (Int, Event) => Watched.Action private type WatchOnEvent = (Int, Event) => Watch.Action
/** /**
* Contains all of the user defined settings that will be used to build a [[Callbacks]] * Contains all of the user defined settings that will be used to build a [[Callbacks]]
* instance that is used to produce the arguments to [[Watched.watch]]. The * instance that is used to produce the arguments to [[Watch.apply]]. The
* callback settings (e.g. onEvent or onInputEvent) come in two forms: those that return a * callback settings (e.g. onEvent or onInputEvent) come in two forms: those that return a
* function from [[Arguments]] => F for some function type `F` and those that directly return a function, e.g. * function from [[Arguments]] => F for some function type `F` and those that directly return a function, e.g.
* `(Int, Boolean) => Watched.Action`. The former are a low level interface that will usually * `(Int, Boolean) => Watch.Action`. The former are a low level interface that will usually
* be unspecified and automatically filled in by [[Continuous.aggregate]]. The latter are * be unspecified and automatically filled in by [[Continuous.aggregate]]. The latter are
* intended to be user configurable and will be scoped to the input [[ScopedKey]]. To ensure * intended to be user configurable and will be scoped to the input [[ScopedKey]]. To ensure
* that the scoping makes sense, we first try and extract the setting from the [[ScopedKey]] * that the scoping makes sense, we first try and extract the setting from the [[ScopedKey]]
@ -703,25 +704,25 @@ object Continuous extends DeprecatedContinuous {
implicit extracted: Extracted implicit extracted: Extracted
) { ) {
val antiEntropy: FiniteDuration = val antiEntropy: FiniteDuration =
key.get(Keys.watchAntiEntropy).getOrElse(Watched.defaultAntiEntropy) key.get(Keys.watchAntiEntropy).getOrElse(Watch.defaultAntiEntropy)
val antiEntropyRetentionPeriod: FiniteDuration = val antiEntropyRetentionPeriod: FiniteDuration =
key key
.get(Keys.watchAntiEntropyRetentionPeriod) .get(Keys.watchAntiEntropyRetentionPeriod)
.getOrElse(Watched.defaultAntiEntropyRetentionPeriod) .getOrElse(Watch.defaultAntiEntropyRetentionPeriod)
val deletionQuarantinePeriod: FiniteDuration = val deletionQuarantinePeriod: FiniteDuration =
key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watched.defaultDeletionQuarantinePeriod) key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod)
val inputHandler: Option[InputStream => Watched.Action] = key.get(Keys.watchInputHandler) val inputHandler: Option[InputStream => Watch.Action] = key.get(Keys.watchInputHandler)
val inputParser: Parser[Watched.Action] = val inputParser: Parser[Watch.Action] =
key.get(Keys.watchInputParser).getOrElse(Watched.defaultInputParser) key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser)
val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info) val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info)
val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {}) val onEnter: () => Unit = key.get(Keys.watchOnEnter).getOrElse(() => {})
val onEvent: Option[Arguments => Event => Watched.Action] = key.get(Keys.watchOnEvent) val onEvent: Option[Arguments => Event => Watch.Action] = key.get(Keys.watchOnEvent)
val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {}) val onExit: () => Unit = key.get(Keys.watchOnExit).getOrElse(() => {})
val onInputEvent: Option[WatchOnEvent] = key.get(Keys.watchOnInputEvent) val onInputEvent: Option[WatchOnEvent] = key.get(Keys.watchOnInputEvent)
val onIteration: Option[Int => Watched.Action] = key.get(Keys.watchOnIteration) val onIteration: Option[Int => Watch.Action] = key.get(Keys.watchOnIteration)
val onMetaBuildEvent: Option[WatchOnEvent] = key.get(Keys.watchOnMetaBuildEvent) val onMetaBuildEvent: Option[WatchOnEvent] = key.get(Keys.watchOnMetaBuildEvent)
val onStart: Option[Arguments => () => Watched.Action] = key.get(Keys.watchOnStart) val onStart: Option[Arguments => () => Watch.Action] = key.get(Keys.watchOnStart)
val onTermination: Option[(Watched.Action, String, Int, State) => State] = val onTermination: Option[(Watch.Action, String, Int, State) => State] =
key.get(Keys.watchOnTermination) key.get(Keys.watchOnTermination)
val onTrigger: Option[Arguments => Event => Unit] = key.get(Keys.watchOnTrigger) val onTrigger: Option[Arguments => Event => Unit] = key.get(Keys.watchOnTrigger)
val onTriggerEvent: Option[WatchOnEvent] = key.get(Keys.watchOnTriggerEvent) val onTriggerEvent: Option[WatchOnEvent] = key.get(Keys.watchOnTriggerEvent)
@ -758,12 +759,12 @@ object Continuous extends DeprecatedContinuous {
def arguments(logger: Logger): Arguments = new Arguments(logger, inputs, triggers) def arguments(logger: Logger): Arguments = new Arguments(logger, inputs, triggers)
} }
private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some { private def getStartMessage(key: ScopedKey[_])(implicit e: Extracted): StartMessage = Some {
lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watched.defaultStartWatch) lazy val default = key.get(Keys.watchStartMessage).getOrElse(Watch.defaultStartWatch)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))
} }
private def getTriggerMessage(key: ScopedKey[_])(implicit e: Extracted): TriggerMessage = Some { private def getTriggerMessage(key: ScopedKey[_])(implicit e: Extracted): TriggerMessage = Some {
lazy val default = lazy val default =
key.get(Keys.watchTriggeredMessage).getOrElse(Watched.defaultOnTriggerMessage) key.get(Keys.watchTriggeredMessage).getOrElse(Watch.defaultOnTriggerMessage)
key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default)) key.get(deprecatedWatchingMessage).map(Left(_)).getOrElse(Right(default))
} }

View File

@ -20,47 +20,47 @@ import sbt.io.Glob
private[sbt] sealed trait GlobLister extends Any { private[sbt] sealed trait GlobLister extends Any {
/** /**
* Get the sources described this [[GlobLister]]. * Get the sources described this `GlobLister`.
* *
* @param repository the [[FileTree.Repository]] to delegate file i/o. * @param repository the [[FileTree.Repository]] to delegate file i/o.
* @return the files described by this [[GlobLister]]. * @return the files described by this `GlobLister`.
*/ */
def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] def all(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)]
/** /**
* Get the unique sources described this [[GlobLister]]. * Get the unique sources described this `GlobLister`.
* *
* @param repository the [[FileTree.Repository]] to delegate file i/o. * @param repository the [[FileTree.Repository]] to delegate file i/o.
* @return the files described by this [[GlobLister]] with any duplicates removed. * @return the files described by this `GlobLister` with any duplicates removed.
*/ */
def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)] def unique(implicit repository: FileTree.Repository): Seq[(Path, FileAttributes)]
} }
/** /**
* Provides implicit definitions to provide a [[GlobLister]] given a Glob or * Provides implicit definitions to provide a `GlobLister` given a Glob or
* Traversable[Glob]. * Traversable[Glob].
*/ */
object GlobLister extends GlobListers private[sbt] object GlobLister extends GlobListers
/** /**
* Provides implicit definitions to provide a [[GlobLister]] given a Glob or * Provides implicit definitions to provide a `GlobLister` given a Glob or
* Traversable[Glob]. * Traversable[Glob].
*/ */
private[sbt] trait GlobListers { private[sbt] trait GlobListers {
import GlobListers._ import GlobListers._
/** /**
* Generate a [[GlobLister]] given a particular [[Glob]]s. * Generate a GlobLister given a particular [[Glob]]s.
* *
* @param source the input Glob * @param source the input Glob
*/ */
implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil) implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil)
/** /**
* Generate a [[GlobLister]] given a collection of Globs. If the input collection type * Generate a GlobLister given a collection of Globs. If the input collection type
* preserves uniqueness, e.g. `Set[Glob]`, then the output of [[GlobLister.all]] will be * preserves uniqueness, e.g. `Set[Glob]`, then the output of `GlobLister.all` will be
* the unique source list. Otherwise duplicates are possible in all and it is necessary to call * the unique source list. Otherwise duplicates are possible in all and it is necessary to call
* [[GlobLister.unique]] to de-duplicate the files. * `GlobLister.unique` to de-duplicate the files.
* *
* @param sources the collection of sources * @param sources the collection of sources
* @tparam T the source collection type * @tparam T the source collection type
@ -71,7 +71,7 @@ private[sbt] trait GlobListers {
private[internal] object GlobListers { private[internal] object GlobListers {
/** /**
* Implements [[GlobLister]] given a collection of Globs. If the input collection type * Implements `GlobLister` given a collection of Globs. If the input collection type
* preserves uniqueness, e.g. `Set[Glob]`, then the output will be the unique source list. * preserves uniqueness, e.g. `Set[Glob]`, then the output will be the unique source list.
* Otherwise duplicates are possible. * Otherwise duplicates are possible.
* *

View File

@ -12,8 +12,8 @@ import java.nio.file.{ Files, Path }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import org.scalatest.{ FlatSpec, Matchers } import org.scalatest.{ FlatSpec, Matchers }
import sbt.Watched._ import sbt.Watch.{ NullLogger, _ }
import sbt.WatchedSpec._ import sbt.WatchSpec._
import sbt.internal.FileAttributes import sbt.internal.FileAttributes
import sbt.io.FileEventMonitor.Event import sbt.io.FileEventMonitor.Event
import sbt.io._ import sbt.io._
@ -23,18 +23,18 @@ import sbt.util.Logger
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
class WatchedSpec extends FlatSpec with Matchers { class WatchSpec extends FlatSpec with Matchers {
private type NextAction = () => Watched.Action private type NextAction = () => Watch.Action
private def watch(task: Task, callbacks: (NextAction, NextAction)): Watched.Action = private def watch(task: Task, callbacks: (NextAction, NextAction)): Watch.Action =
Watched.watch(task, callbacks._1, callbacks._2) Watch(task, callbacks._1, callbacks._2)
object TestDefaults { object TestDefaults {
def callbacks( def callbacks(
inputs: Seq[Glob], inputs: Seq[Glob],
fileEventMonitor: Option[FileEventMonitor[FileAttributes]] = None, fileEventMonitor: Option[FileEventMonitor[FileAttributes]] = None,
logger: Logger = NullLogger, logger: Logger = NullLogger,
parseEvent: () => Action = () => Ignore, parseEvent: () => Watch.Action = () => Ignore,
onStartWatch: () => Action = () => CancelWatch: Action, onStartWatch: () => Watch.Action = () => CancelWatch: Watch.Action,
onWatchEvent: Event[FileAttributes] => Action = _ => Ignore, onWatchEvent: Event[FileAttributes] => Watch.Action = _ => Ignore,
triggeredMessage: Event[FileAttributes] => Option[String] = _ => None, triggeredMessage: Event[FileAttributes] => Option[String] = _ => None,
watchingMessage: () => Option[String] = () => None watchingMessage: () => Option[String] = () => None
): (NextAction, NextAction) = { ): (NextAction, NextAction) = {
@ -57,7 +57,7 @@ class WatchedSpec extends FlatSpec with Matchers {
val onTrigger: Event[FileAttributes] => Unit = event => { val onTrigger: Event[FileAttributes] => Unit = event => {
triggeredMessage(event).foreach(logger.info(_)) triggeredMessage(event).foreach(logger.info(_))
} }
val onStart: () => Watched.Action = () => { val onStart: () => Watch.Action = () => {
watchingMessage().foreach(logger.info(_)) watchingMessage().foreach(logger.info(_))
onStartWatch() onStartWatch()
} }
@ -66,7 +66,7 @@ class WatchedSpec extends FlatSpec with Matchers {
val fileActions = monitor.poll(10.millis).map { e: Event[FileAttributes] => val fileActions = monitor.poll(10.millis).map { e: Event[FileAttributes] =>
onWatchEvent(e) match { onWatchEvent(e) match {
case Trigger => onTrigger(e); Trigger case Trigger => onTrigger(e); Trigger
case act => act case action => action
} }
} }
(inputAction +: fileActions).min (inputAction +: fileActions).min
@ -86,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers {
} }
def getCount: Int = count.get() def getCount: Int = count.get()
} }
"Watched.watch" should "stop" in IO.withTemporaryDirectory { dir => "Watch" should "stop" in IO.withTemporaryDirectory { dir =>
val task = new Task val task = new Task
watch(task, TestDefaults.callbacks(inputs = Seq(dir.toRealPath ** AllPassFilter))) shouldBe CancelWatch watch(task, TestDefaults.callbacks(inputs = Seq(dir.toRealPath ** AllPassFilter))) shouldBe CancelWatch
} }
@ -168,7 +168,7 @@ class WatchedSpec extends FlatSpec with Matchers {
} }
} }
object WatchedSpec { object WatchSpec {
implicit class FileOps(val f: File) { implicit class FileOps(val f: File) {
def toRealPath: File = f.toPath.toRealPath().toFile def toRealPath: File = f.toPath.toRealPath().toFile
} }

View File

@ -10,4 +10,4 @@ checkStringValue := checkStringValueImpl.evaluated
setStringValue / watchTriggers := baseDirectory.value * "string.txt" :: Nil setStringValue / watchTriggers := baseDirectory.value * "string.txt" :: Nil
watchOnEvent := { _ => _ => Watched.CancelWatch } watchOnEvent := { _ => _ => Watch.CancelWatch }

View File

@ -31,10 +31,10 @@ object Build {
setStringValueImpl.evaluated setStringValueImpl.evaluated
}, },
checkStringValue := checkStringValueImpl.evaluated, checkStringValue := checkStringValueImpl.evaluated,
watchOnEvent := { _ => _ => Watched.CancelWatch } watchOnEvent := { _ => _ => Watch.CancelWatch }
) )
lazy val bar = project.settings(fileInputs in setStringValue += baseDirectory.value * "foo.txt") lazy val bar = project.settings(fileInputs in setStringValue += baseDirectory.value * "foo.txt")
lazy val root = (project in file(".")).aggregate(foo, bar).settings( lazy val root = (project in file(".")).aggregate(foo, bar).settings(
watchOnEvent := { _ => _ => Watched.CancelWatch } watchOnEvent := { _ => _ => Watch.CancelWatch }
) )
} }

View File

@ -30,9 +30,9 @@ object Build {
setStringValueImpl.evaluated setStringValueImpl.evaluated
}, },
checkStringValue := checkStringValueImpl.evaluated, checkStringValue := checkStringValueImpl.evaluated,
watchOnTriggerEvent := { (_, _) => Watched.CancelWatch }, watchOnTriggerEvent := { (_, _) => Watch.CancelWatch },
watchOnInputEvent := { (_, _) => Watched.CancelWatch }, watchOnInputEvent := { (_, _) => Watch.CancelWatch },
Compile / compile / watchOnStart := { _ => () => Watched.CancelWatch }, Compile / compile / watchOnStart := { _ => () => Watch.CancelWatch },
checkTriggers := { checkTriggers := {
val actual = (Compile / compile / transitiveTriggers).value.toSet val actual = (Compile / compile / transitiveTriggers).value.toSet
val base = baseDirectory.value.getParentFile val base = baseDirectory.value.getParentFile
@ -83,7 +83,7 @@ object Build {
checkGlobs := checkGlobsImpl.value checkGlobs := checkGlobsImpl.value
) )
lazy val root = (project in file(".")).aggregate(foo, bar).settings( lazy val root = (project in file(".")).aggregate(foo, bar).settings(
watchOnEvent := { _ => _ => Watched.CancelWatch }, watchOnEvent := { _ => _ => Watch.CancelWatch },
checkTriggers := { checkTriggers := {
val actual = (Compile / compile / transitiveTriggers).value val actual = (Compile / compile / transitiveTriggers).value
val expected: Seq[Glob] = baseDirectory.value * "baz.txt" :: Nil val expected: Seq[Glob] = baseDirectory.value * "baz.txt" :: Nil
@ -91,4 +91,4 @@ object Build {
}, },
checkGlobs := checkGlobsImpl.value checkGlobs := checkGlobsImpl.value
) )
} }

View File

@ -1,9 +1 @@
import sbt.input.parser.Build val root = sbt.input.parser.Build.root
watchInputStream := Build.inputStream
watchStartMessage := { count =>
Build.outputStream.write('\n'.toByte)
Build.outputStream.flush()
Some("default start message")
}

View File

@ -5,15 +5,25 @@ import complete.Parser
import complete.Parser._ import complete.Parser._
import java.io.{ PipedInputStream, PipedOutputStream } import java.io.{ PipedInputStream, PipedOutputStream }
import Keys._
object Build { object Build {
val root = (project in file(".")).settings(
useSuperShell := false,
watchInputStream := inputStream,
watchStartMessage := { count =>
Build.outputStream.write('\n'.toByte)
Build.outputStream.flush()
Some("default start message")
}
)
val outputStream = new PipedOutputStream() val outputStream = new PipedOutputStream()
val inputStream = new PipedInputStream(outputStream) val inputStream = new PipedInputStream(outputStream)
val byeParser: Parser[Watched.Action] = "bye" ^^^ Watched.CancelWatch val byeParser: Parser[Watch.Action] = "bye" ^^^ Watch.CancelWatch
val helloParser: Parser[Watched.Action] = "hello" ^^^ Watched.Ignore val helloParser: Parser[Watch.Action] = "hello" ^^^ Watch.Ignore
// Note that the order is byeParser | helloParser. In general, we want the higher priority // Note that the order is byeParser | helloParser. In general, we want the higher priority
// action to come first because otherwise we would potentially scan past it. // action to come first because otherwise we would potentially scan past it.
val helloOrByeParser: Parser[Watched.Action] = byeParser | helloParser val helloOrByeParser: Parser[Watch.Action] = byeParser | helloParser
val alternativeStartMessage: Int => Option[String] = { _ => val alternativeStartMessage: Int => Option[String] = { _ =>
outputStream.write("xybyexyblahxyhelloxy".getBytes) outputStream.write("xybyexyblahxyhelloxy".getBytes)
outputStream.flush() outputStream.flush()

View File

@ -7,8 +7,8 @@
> set watchInputParser := sbt.input.parser.Build.helloOrByeParser > set watchInputParser := sbt.input.parser.Build.helloOrByeParser
# this should exit because we write "xybyexyblahxyhelloxy" to Build.outputStream. The # this should exit because we write "xybyexyblahxyhelloxy" to Build.outputStream. The
# helloOrByeParser will produce Watched.Ignore and Watched.CancelWatch but the # helloOrByeParser will produce Watch.Ignore and Watch.CancelWatch but the
# Watched.CancelWatch event should win. # Watch.CancelWatch event should win.
> ~ compile > ~ compile
> set watchStartMessage := sbt.input.parser.Build.otherAlternativeStartMessage > set watchStartMessage := sbt.input.parser.Build.otherAlternativeStartMessage

View File

@ -8,6 +8,6 @@ setStringValue := setStringValueImpl.evaluated
checkStringValue := checkStringValueImpl.evaluated checkStringValue := checkStringValueImpl.evaluated
watchOnTriggerEvent := { (_, _) => Watched.CancelWatch } watchOnTriggerEvent := { (_, _) => Watch.CancelWatch }
watchOnInputEvent := { (_, _) => Watched.CancelWatch } watchOnInputEvent := { (_, _) => Watch.CancelWatch }
watchOnMetaBuildEvent := { (_, _) => Watched.CancelWatch } watchOnMetaBuildEvent := { (_, _) => Watch.CancelWatch }

View File

@ -1,4 +1,4 @@
val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded") val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded")
checkReloaded := { () } checkReloaded := { () }
watchOnIteration := { _ => Watched.CancelWatch } watchOnIteration := { _ => Watch.CancelWatch }

View File

@ -1 +1 @@
watchOnStart := { _ => () => Watched.Reload } watchOnStart := { _ => () => Watch.Reload }

View File

@ -4,11 +4,7 @@ import scala.util.Try
object Count { object Count {
private var count = 0 private var count = 0
def get: Int = count def get: Int = count
def increment(): Unit = { def increment(): Unit = count += 1
count += 1 def reset(): Unit = count = 0
}
def reset(): Unit = {
count = 0
}
def reloadCount(file: File): Int = Try(IO.read(file).toInt).getOrElse(0) def reloadCount(file: File): Int = Try(IO.read(file).toInt).getOrElse(0)
} }

View File

@ -1,4 +1,4 @@
# verify that reloading occurs if watchOnStart returns Watched.Reload # verify that reloading occurs if watchOnStart returns Watch.Reload
$ copy-file changes/extra.sbt extra.sbt $ copy-file changes/extra.sbt extra.sbt
> ~compile > ~compile
@ -6,19 +6,19 @@ $ copy-file changes/extra.sbt extra.sbt
# verify that the watch terminates when we reach the specified count # verify that the watch terminates when we reach the specified count
> resetCount > resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) Watched.CancelWatch else Watched.Ignore } > set watchOnIteration := { (count: Int) => if (count == 2) Watch.CancelWatch else Watch.Ignore }
> ~compile > ~compile
> checkCount 2 > checkCount 2
# verify that the watch terminates and returns an error when we reach the specified count # verify that the watch terminates and returns an error when we reach the specified count
> resetCount > resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) new Watched.HandleError(new Exception("")) else Watched.Ignore } > set watchOnIteration := { (count: Int) => if (count == 2) new Watch.HandleError(new Exception("")) else Watch.Ignore }
# Returning Watched.HandleError causes the '~' command to fail # Returning Watch.HandleError causes the '~' command to fail
-> ~compile -> ~compile
> checkCount 2 > checkCount 2
# verify that a re-build is triggered when we reach the specified count # verify that a re-build is triggered when we reach the specified count
> resetCount > resetCount
> set watchOnIteration := { (count: Int) => if (count == 2) Watched.Trigger else if (count == 3) Watched.CancelWatch else Watched.Ignore } > set watchOnIteration := { (count: Int) => if (count == 2) Watch.Trigger else if (count == 3) Watch.CancelWatch else Watch.Ignore }
> ~compile > ~compile
> checkCount 3 > checkCount 3

View File

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

View File

@ -25,9 +25,9 @@ object Build {
Some("watching") Some("watching")
}, },
watchOnTriggerEvent := { (f, e) => watchOnTriggerEvent := { (f, e) =>
if (reloadFile.value.exists) Watched.CancelWatch else { if (reloadFile.value.exists) Watch.CancelWatch else {
IO.touch(reloadFile.value, true) IO.touch(reloadFile.value, true)
Watched.Reload Watch.Reload
} }
} }
) )

View File

@ -1,7 +1,7 @@
# this tests that if the watch _task_ is able to reload the project # this tests that if the watch _task_ is able to reload the project
# the original version of the build will only return Watched.Reload for trigger events while the # the original version of the build will only return Watch.Reload for trigger events while the
# updated version will return Watched.CancelWatch. If this test exits, it more or less works. # updated version will return Watch.CancelWatch. If this test exits, it more or less works.
$ copy-file changes/Build.scala project/Build.scala $ copy-file changes/Build.scala project/Build.scala
# setStringValue has foo.txt as a watch source so running that command should first trigger a # setStringValue has foo.txt as a watch source so running that command should first trigger a