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
import java.io.{ File, InputStream }
import java.io.File
import java.nio.file.FileSystems
import sbt.BasicCommandStrings.ContinuousExecutePrefix
import sbt.internal.LabeledFunctions._
import sbt.internal.{ FileAttributes, LegacyWatched }
import sbt.internal.LegacyWatched
import sbt.internal.io.{ EventMonitor, Source, WatchState }
import sbt.internal.util.Types.const
import sbt.internal.util.complete.DefaultParsers._
import sbt.internal.util.complete.Parser
import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update }
import sbt.internal.util.AttributeKey
import sbt.io._
import sbt.util.{ Level, Logger }
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.Properties
import scala.util.control.NonFatal
@deprecated("Watched is no longer used to implement continuous execution", "1.3.0")
trait Watched {
@ -58,283 +51,13 @@ trait 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")
type WatchSource = Source
private[sbt] type OnTermination = (Action, String, State) => State
private[sbt] type OnEnter = () => Unit
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 =
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"
@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 =
(() => createWatchService()).label("Watched.newWatchService")
def createWatchService(pollDelay: FiniteDuration): WatchService = {
@ -479,9 +121,9 @@ object Watched {
((ws: WatchState) => projectOnWatchMessage(projectId)(ws.count).get)
.label("Watched.projectWatchingMessage")
@deprecated("unused", "1.3.0")
def projectOnWatchMessage(project: String): Int => Option[String] =
((count: Int) => Some(s"$count. ${waitMessage(s" in project $project")}"))
.label("Watched.projectOnWatchMessage")
def projectOnWatchMessage(project: String): Int => Option[String] = { (count: Int) =>
Some(s"$count. ${waitMessage(s" in project $project")}")
}.label("Watched.projectOnWatchMessage")
@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
@ -522,7 +164,8 @@ object Watched {
@deprecated("Use defaultStartWatch in conjunction with the watchStartMessage key", "1.3.0")
val defaultWatchingMessage: WatchState => String =
((ws: WatchState) => defaultStartWatch(ws.count).get).label("Watched.defaultWatchingMessage")
((ws: WatchState) => s"${ws.count}. ${waitMessage("")} ")
.label("Watched.projectWatchingMessage")
@deprecated(
"Use defaultOnTriggerMessage in conjunction with the watchTriggeredMessage key",
"1.3.0"

View File

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

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
import java.io.IOException
import java.net.Socket
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic._
import scala.collection.mutable.ListBuffer
import scala.annotation.tailrec
import BasicKeys.{
autoStartServer,
fullServerHandlers,
logLevel,
serverAuthentication,
serverConnectionType,
serverHost,
serverLogLevel,
serverPort
}
import java.net.Socket
import sbt.Watched.NullLogger
import sbt.BasicKeys._
import sbt.Watch.NullLogger
import sbt.internal.langserver.{ LogMessageParams, MessageType }
import sbt.internal.server._
import sbt.internal.util.codec.JValueFormats
import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent }
import sbt.io.syntax._
import sbt.io.{ Hash, IO }
import sbt.protocol.{ EventMessage, ExecStatusEvent }
import sbt.util.{ Level, LogExchange, Logger }
import sjsonnew.JsonFormat
import sjsonnew.shaded.scalajson.ast.unsafe._
import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer
import scala.concurrent.Await
import scala.concurrent.duration._
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),

View File

@ -20,13 +20,13 @@ import sbt.BasicCommandStrings.{
import sbt.BasicCommands.otherCommandParser
import sbt.Def._
import sbt.Scope.Global
import sbt.Watched.Monitor
import sbt.internal.FileManagement.FileTreeRepositoryOps
import sbt.internal.LabeledFunctions._
import sbt.internal.io.WatchState
import sbt.internal.util.Types.const
import sbt.internal.util.complete.Parser._
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.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.
* @param parser the parser
* @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 any = matched(Parsers.any.*)
val fullParser = any ~> parser ~ any
inputStream =>
parse(inputStream, builder, fullParser)
((inputStream: InputStream) => parse(inputStream, builder, fullParser))
.label("Continuous.defaultInputHandler")
}
/**
@ -254,7 +254,7 @@ object Continuous extends DeprecatedContinuous {
command: String,
count: Int,
isCommand: Boolean
): State = Watched.withCharBufferedStdIn { in =>
): State = Watch.withCharBufferedStdIn { in =>
val duped = new DupedInputStream(in)
setup(state.put(DupedSystemIn, duped), command) { (s, commands, valid, invalid) =>
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
// additional times whenever the state transition callbacks return Watched.Trigger.
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)
} finally callbacks.onExit()
} else {
@ -315,11 +315,11 @@ object Continuous extends DeprecatedContinuous {
}
private class Callbacks(
val nextEvent: () => Watched.Action,
val nextEvent: () => Watch.Action,
val onEnter: () => Unit,
val onExit: () => Unit,
val onStart: () => Watched.Action,
val onTermination: (Watched.Action, String, Int, State) => State
val onStart: () => Watch.Action,
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
* 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
* to each of the individual callbacks. For the callbacks that return a [[Watched.Action]],
* the aggregated callback will select the minimum [[Watched.Action]] returned where the ordering
* is such that the highest priority [[Watched.Action]] have the lowest values. Finally, to
* to each of the individual callbacks. For the callbacks that return a [[Watch.Action]],
* the aggregated callback will select the minimum [[Watch.Action]] returned where the ordering
* 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
* 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
* [[Watched.Ignore]], which is the lowest priority [[Watched.Action]].
* [[Watch.Action]] returned by the parsers (when the parsers fail, they just return
* [[Watch.Ignore]], which is the lowest priority [[Watch.Action]].
*
* @param configs the [[Config]] instances
* @param rawLogger the default sbt logger instance
* @param state the current state
* @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(
configs: Seq[Config],
@ -355,11 +355,11 @@ object Continuous extends DeprecatedContinuous {
): Callbacks = {
val logger = setLevel(rawLogger, configs.map(_.watchSettings.logLevel).min, state)
val onEnter = () => configs.foreach(_.watchSettings.onEnter())
val onStart: () => Watched.Action = getOnStart(configs, logger, count)
val nextInputEvent: () => Watched.Action = parseInputEvents(configs, state, inputStream, logger)
val (nextFileEvent, cleanupFileMonitor): (() => Watched.Action, () => Unit) =
val onStart: () => Watch.Action = getOnStart(configs, logger, count)
val nextInputEvent: () => Watch.Action = parseInputEvents(configs, state, inputStream, logger)
val (nextFileEvent, cleanupFileMonitor): (() => Watch.Action, () => Unit) =
getFileEvents(configs, logger, state, count)
val nextEvent: () => Watched.Action =
val nextEvent: () => Watch.Action =
combineInputAndFileEvents(nextInputEvent, nextFileEvent, logger)
val onExit = () => {
cleanupFileMonitor()
@ -372,7 +372,7 @@ object Continuous extends DeprecatedContinuous {
private def getOnTermination(
configs: Seq[Config],
isCommand: Boolean
): (Watched.Action, String, Int, State) => State = {
): (Watch.Action, String, Int, State) => State = {
configs.flatMap(_.watchSettings.onTermination).distinct match {
case Seq(head, tail @ _*) =>
tail.foldLeft(head) {
@ -381,7 +381,7 @@ object Continuous extends DeprecatedContinuous {
configOnTermination(action, cmd, count, onTermination(action, cmd, count, state))
}
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],
logger: Logger,
count: AtomicInteger
): () => Watched.Action = {
): () => Watch.Action = {
val f = configs.map { params =>
val ws = params.watchSettings
ws.onStart.map(_.apply(params.arguments(logger))).getOrElse { () =>
@ -398,10 +398,10 @@ object Continuous extends DeprecatedContinuous {
ws.startMessage match {
case Some(Left(sm)) => logger.info(sm(params.watchState(count.get())))
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
// 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
}
}
@ -418,7 +418,7 @@ object Continuous extends DeprecatedContinuous {
logger: Logger,
state: State,
count: AtomicInteger,
)(implicit extracted: Extracted): (() => Watched.Action, () => Unit) = {
)(implicit extracted: Extracted): (() => Watch.Action, () => Unit) = {
val trackMetaBuild = configs.forall(_.watchSettings.trackMetaBuild)
val buildGlobs =
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,
* 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 ws = params.watchSettings
ws.onTrigger
@ -449,38 +449,39 @@ object Continuous extends DeprecatedContinuous {
}
event: 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 ws = params.watchSettings
val oe = ws.onEvent
.map(_.apply(params.arguments(logger)))
.getOrElse {
val onInputEvent = ws.onInputEvent.getOrElse(Watched.trigger)
val onTriggerEvent = ws.onTriggerEvent.getOrElse(Watched.trigger)
val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watched.ifChanged(Watched.Reload))
val onInputEvent = ws.onInputEvent.getOrElse(defaultTrigger)
val onTriggerEvent = ws.onTriggerEvent.getOrElse(defaultTrigger)
val onMetaBuildEvent = ws.onMetaBuildEvent.getOrElse(Watch.ifChanged(Watch.Reload))
val inputFilter = params.inputs.toEntryFilter
val triggerFilter = params.triggers.toEntryFilter
event: Event =>
val c = count.get()
Seq[Watched.Action](
if (inputFilter(event.entry)) onInputEvent(c, event) else Watched.Ignore,
if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watched.Ignore,
if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watched.Ignore
Seq[Watch.Action](
if (inputFilter(event.entry)) onInputEvent(c, event) else Watch.Ignore,
if (triggerFilter(event.entry)) onTriggerEvent(c, event) else Watch.Ignore,
if (buildFilter(event.entry)) onMetaBuildEvent(c, event) else Watch.Ignore
).min
}
event: Event =>
event -> (oe(event) match {
case Watched.Trigger => onTrigger(event)
case a => a
case Watch.Trigger => onTrigger(event)
case a => a
})
}
event: Event =>
f.view.map(_.apply(event)).minBy(_._2)
}
val monitor: Monitor = new FileEventMonitor[FileAttributes] {
val monitor: FileEventMonitor[FileAttributes] = new FileEventMonitor[FileAttributes] {
private def setup(
monitor: FileEventMonitor[FileAttributes],
globs: Seq[Glob]
@ -527,11 +528,11 @@ object Continuous extends DeprecatedContinuous {
)
(() => {
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)
logger.debug(s"Received file event actions: ${actions.mkString(", ")}. Returning: $min")
min._2
} else Watched.Ignore
} else Watch.Ignore
}, () => 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
* 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,
* 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.
* 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
* 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
* 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
* 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(
configs: Seq[Config],
@ -557,15 +558,15 @@ object Continuous extends DeprecatedContinuous {
logger: Logger
)(
implicit extracted: Extracted
): () => Watched.Action = {
): () => Watch.Action = {
/*
* 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 attempt to parse again.
*/
type ActionParser = String => Watched.Action
type ActionParser = String => Watch.Action
// 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
// 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
@ -581,7 +582,7 @@ object Continuous extends DeprecatedContinuous {
val systemInBuilder = new StringBuilder
def inputStream(string: String): InputStream = new ByteArrayInputStream(string.getBytes)
// 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)
val alternative = c.watchSettings.inputStream
.map { inputStreamKey =>
@ -590,7 +591,7 @@ object Continuous extends DeprecatedContinuous {
() =>
handler(is)
}
.getOrElse(() => Watched.Ignore)
.getOrElse(() => Watch.Ignore)
(string: String) =>
(default(string) :: alternative() :: Nil).min
}
@ -599,32 +600,32 @@ object Continuous extends DeprecatedContinuous {
val stringBuilder = new StringBuilder
while (inputStream.available > 0) stringBuilder += inputStream.read().toChar
val newBytes = stringBuilder.toString
val parse: ActionParser => Watched.Action = parser => parser(newBytes)
val allEvents = inputHandlers.map(parse).filterNot(_ == Watched.Ignore)
if (allEvents.exists(_ != Watched.Ignore)) {
val parse: ActionParser => Watch.Action = parser => parser(newBytes)
val allEvents = inputHandlers.map(parse).filterNot(_ == Watch.Ignore)
if (allEvents.exists(_ != Watch.Ignore)) {
val res = allEvents.min
logger.debug(s"Received input events: ${allEvents mkString ","}. Taking $res")
res
} else Watched.Ignore
} else Watch.Ignore
}
}
private def combineInputAndFileEvents(
nextInputEvent: () => Watched.Action,
nextFileEvent: () => Watched.Action,
nextInputEvent: () => Watch.Action,
nextFileEvent: () => Watch.Action,
logger: Logger
): () => Watched.Action = () => {
val Seq(inputEvent: Watched.Action, fileEvent: Watched.Action) =
): () => Watch.Action = () => {
val Seq(inputEvent: Watch.Action, fileEvent: Watch.Action) =
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 =
s"Received input event: $inputEvent." +
(if (inputEvent != min) s" Dropping in favor of file event: $min" else "")
lazy val fileMessage =
s"Received file event: $fileEvent." +
(if (fileEvent != min) s" Dropping in favor of input event: $min" else "")
if (inputEvent != Watched.Ignore) logger.debug(inputMessage)
if (fileEvent != Watched.Ignore) logger.debug(fileMessage)
if (inputEvent != Watch.Ignore) logger.debug(inputMessage)
if (fileEvent != Watch.Ignore) logger.debug(fileMessage)
min
}
@ -632,8 +633,8 @@ object Continuous extends DeprecatedContinuous {
private final def parse(
is: InputStream,
builder: StringBuilder,
parser: Parser[(Watched.Action, String)]
): Watched.Action = {
parser: Parser[(Watch.Action, String)]
): Watch.Action = {
if (is.available > 0) builder += is.read().toChar
Parser.parse(builder.toString, parser) match {
case Right((action, rest)) =>
@ -641,7 +642,7 @@ object Continuous extends DeprecatedContinuous {
builder ++= rest
action
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]]
* 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
* 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
* 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]]
@ -703,25 +704,25 @@ object Continuous extends DeprecatedContinuous {
implicit extracted: Extracted
) {
val antiEntropy: FiniteDuration =
key.get(Keys.watchAntiEntropy).getOrElse(Watched.defaultAntiEntropy)
key.get(Keys.watchAntiEntropy).getOrElse(Watch.defaultAntiEntropy)
val antiEntropyRetentionPeriod: FiniteDuration =
key
.get(Keys.watchAntiEntropyRetentionPeriod)
.getOrElse(Watched.defaultAntiEntropyRetentionPeriod)
.getOrElse(Watch.defaultAntiEntropyRetentionPeriod)
val deletionQuarantinePeriod: FiniteDuration =
key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watched.defaultDeletionQuarantinePeriod)
val inputHandler: Option[InputStream => Watched.Action] = key.get(Keys.watchInputHandler)
val inputParser: Parser[Watched.Action] =
key.get(Keys.watchInputParser).getOrElse(Watched.defaultInputParser)
key.get(Keys.watchDeletionQuarantinePeriod).getOrElse(Watch.defaultDeletionQuarantinePeriod)
val inputHandler: Option[InputStream => Watch.Action] = key.get(Keys.watchInputHandler)
val inputParser: Parser[Watch.Action] =
key.get(Keys.watchInputParser).getOrElse(Watch.defaultInputParser)
val logLevel: Level.Value = key.get(Keys.watchLogLevel).getOrElse(Level.Info)
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 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 onStart: Option[Arguments => () => Watched.Action] = key.get(Keys.watchOnStart)
val onTermination: Option[(Watched.Action, String, Int, State) => State] =
val onStart: Option[Arguments => () => Watch.Action] = key.get(Keys.watchOnStart)
val onTermination: Option[(Watch.Action, String, Int, State) => State] =
key.get(Keys.watchOnTermination)
val onTrigger: Option[Arguments => Event => Unit] = key.get(Keys.watchOnTrigger)
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)
}
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))
}
private def getTriggerMessage(key: ScopedKey[_])(implicit e: Extracted): TriggerMessage = Some {
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))
}

View File

@ -20,47 +20,47 @@ import sbt.io.Glob
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.
* @return the files described by this [[GlobLister]].
* @return the files described by this `GlobLister`.
*/
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.
* @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)]
}
/**
* Provides implicit definitions to provide a [[GlobLister]] given a Glob or
* Provides implicit definitions to provide a `GlobLister` given a Glob or
* 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].
*/
private[sbt] trait GlobListers {
import GlobListers._
/**
* Generate a [[GlobLister]] given a particular [[Glob]]s.
* Generate a GlobLister given a particular [[Glob]]s.
*
* @param source the input Glob
*/
implicit def fromGlob(source: Glob): GlobLister = new impl(source :: Nil)
/**
* 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
* 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
* 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
* @tparam T the source collection type
@ -71,7 +71,7 @@ private[sbt] trait 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.
* Otherwise duplicates are possible.
*

View File

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

View File

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

View File

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

View File

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

View File

@ -5,15 +5,25 @@ import complete.Parser
import complete.Parser._
import java.io.{ PipedInputStream, PipedOutputStream }
import Keys._
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 inputStream = new PipedInputStream(outputStream)
val byeParser: Parser[Watched.Action] = "bye" ^^^ Watched.CancelWatch
val helloParser: Parser[Watched.Action] = "hello" ^^^ Watched.Ignore
val byeParser: Parser[Watch.Action] = "bye" ^^^ Watch.CancelWatch
val helloParser: Parser[Watch.Action] = "hello" ^^^ Watch.Ignore
// 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.
val helloOrByeParser: Parser[Watched.Action] = byeParser | helloParser
val helloOrByeParser: Parser[Watch.Action] = byeParser | helloParser
val alternativeStartMessage: Int => Option[String] = { _ =>
outputStream.write("xybyexyblahxyhelloxy".getBytes)
outputStream.flush()

View File

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

View File

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

View File

@ -1,4 +1,4 @@
val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded")
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 {
private var count = 0
def get: Int = count
def increment(): Unit = {
count += 1
}
def reset(): Unit = {
count = 0
}
def increment(): Unit = count += 1
def reset(): Unit = count = 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
> ~compile
@ -6,19 +6,19 @@ $ copy-file changes/extra.sbt extra.sbt
# verify that the watch terminates when we reach the specified count
> 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
> 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 Watched.HandleError(new Exception("")) else Watched.Ignore }
# Returning Watched.HandleError causes the '~' command to fail
> set watchOnIteration := { (count: Int) => if (count == 2) new Watch.HandleError(new Exception("")) else 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) 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
> checkCount 3

View File

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

View File

@ -25,9 +25,9 @@ object Build {
Some("watching")
},
watchOnTriggerEvent := { (f, e) =>
if (reloadFile.value.exists) Watched.CancelWatch else {
if (reloadFile.value.exists) Watch.CancelWatch else {
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
# the original version of the build will only return Watched.Reload for trigger events while the
# updated version will return Watched.CancelWatch. If this test exits, it more or less works.
# the original version of the build will only return Watch.Reload for trigger events while the
# updated version will return Watch.CancelWatch. If this test exits, it more or less works.
$ copy-file changes/Build.scala project/Build.scala
# setStringValue has foo.txt as a watch source so running that command should first trigger a