From 40d8d8876d93fc2d2f01308f070502cd13f01b61 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 27 Mar 2019 20:42:10 -0700 Subject: [PATCH] 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. --- main-command/src/main/scala/sbt/Watched.scala | 375 +----------------- main/src/main/scala/sbt/Defaults.scala | 20 +- main/src/main/scala/sbt/Keys.scala | 18 +- main/src/main/scala/sbt/Watch.scala | 373 +++++++++++++++++ .../scala/sbt/internal/CommandExchange.scala | 36 +- .../main/scala/sbt/internal/Continuous.scala | 161 ++++---- .../main/scala/sbt/internal/GlobLister.scala | 24 +- .../src/test/scala/sbt/WatchSpec.scala | 26 +- .../sbt-test/watch/command-parser/build.sbt | 2 +- .../watch/custom-config/project/Build.scala | 6 +- .../input-aggregation/project/Build.scala | 10 +- sbt/src/sbt-test/watch/input-parser/build.sbt | 10 +- .../watch/input-parser/project/Build.scala | 16 +- sbt/src/sbt-test/watch/input-parser/test | 4 +- .../sbt-test/watch/legacy-sources/build.sbt | 6 +- .../watch/on-start-watch/changes/extra.sbt | 2 +- .../sbt-test/watch/on-start-watch/extra.sbt | 2 +- .../watch/on-start-watch/project/Count.scala | 8 +- sbt/src/sbt-test/watch/on-start-watch/test | 10 +- .../sbt-test/watch/task/changes/Build.scala | 4 +- .../sbt-test/watch/task/project/Build.scala | 4 +- sbt/src/sbt-test/watch/task/test | 4 +- 22 files changed, 563 insertions(+), 558 deletions(-) create mode 100644 main/src/main/scala/sbt/Watch.scala rename main-command/src/test/scala/sbt/WatchedSpec.scala => main/src/test/scala/sbt/WatchSpec.scala (90%) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index abc1f9412..1543a3bba 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -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" diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 516f4d919..0d151743e 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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, ) ) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 0d78d65fc..e25f962cf 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -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. diff --git a/main/src/main/scala/sbt/Watch.scala b/main/src/main/scala/sbt/Watch.scala new file mode 100644 index 000000000..730993d9f --- /dev/null +++ b/main/src/main/scala/sbt/Watch.scala @@ -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") +} diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 5f601a74e..be1a15214 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -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), diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index 277fa968b..ce6eaf86b 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -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)) } diff --git a/main/src/main/scala/sbt/internal/GlobLister.scala b/main/src/main/scala/sbt/internal/GlobLister.scala index 90d0aaa3e..03483b312 100644 --- a/main/src/main/scala/sbt/internal/GlobLister.scala +++ b/main/src/main/scala/sbt/internal/GlobLister.scala @@ -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. * diff --git a/main-command/src/test/scala/sbt/WatchedSpec.scala b/main/src/test/scala/sbt/WatchSpec.scala similarity index 90% rename from main-command/src/test/scala/sbt/WatchedSpec.scala rename to main/src/test/scala/sbt/WatchSpec.scala index 538321c9a..c5139a228 100644 --- a/main-command/src/test/scala/sbt/WatchedSpec.scala +++ b/main/src/test/scala/sbt/WatchSpec.scala @@ -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 } diff --git a/sbt/src/sbt-test/watch/command-parser/build.sbt b/sbt/src/sbt-test/watch/command-parser/build.sbt index 1e26f0e48..dbd347c9b 100644 --- a/sbt/src/sbt-test/watch/command-parser/build.sbt +++ b/sbt/src/sbt-test/watch/command-parser/build.sbt @@ -10,4 +10,4 @@ checkStringValue := checkStringValueImpl.evaluated setStringValue / watchTriggers := baseDirectory.value * "string.txt" :: Nil -watchOnEvent := { _ => _ => Watched.CancelWatch } +watchOnEvent := { _ => _ => Watch.CancelWatch } diff --git a/sbt/src/sbt-test/watch/custom-config/project/Build.scala b/sbt/src/sbt-test/watch/custom-config/project/Build.scala index 2696d5c75..a9acf4e87 100644 --- a/sbt/src/sbt-test/watch/custom-config/project/Build.scala +++ b/sbt/src/sbt-test/watch/custom-config/project/Build.scala @@ -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 } ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala b/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala index eca66f9b5..d99c46b54 100644 --- a/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala +++ b/sbt/src/sbt-test/watch/input-aggregation/project/Build.scala @@ -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 ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/input-parser/build.sbt b/sbt/src/sbt-test/watch/input-parser/build.sbt index 5de1267dc..0e7f77deb 100644 --- a/sbt/src/sbt-test/watch/input-parser/build.sbt +++ b/sbt/src/sbt-test/watch/input-parser/build.sbt @@ -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") -} \ No newline at end of file +val root = sbt.input.parser.Build.root diff --git a/sbt/src/sbt-test/watch/input-parser/project/Build.scala b/sbt/src/sbt-test/watch/input-parser/project/Build.scala index d430bdb76..2bb9dde58 100644 --- a/sbt/src/sbt-test/watch/input-parser/project/Build.scala +++ b/sbt/src/sbt-test/watch/input-parser/project/Build.scala @@ -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() diff --git a/sbt/src/sbt-test/watch/input-parser/test b/sbt/src/sbt-test/watch/input-parser/test index 981496f0b..8169581ba 100644 --- a/sbt/src/sbt-test/watch/input-parser/test +++ b/sbt/src/sbt-test/watch/input-parser/test @@ -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 diff --git a/sbt/src/sbt-test/watch/legacy-sources/build.sbt b/sbt/src/sbt-test/watch/legacy-sources/build.sbt index 5ee39a863..bd3099063 100644 --- a/sbt/src/sbt-test/watch/legacy-sources/build.sbt +++ b/sbt/src/sbt-test/watch/legacy-sources/build.sbt @@ -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 } diff --git a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt index e8b658ba1..36ddd06e3 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/changes/extra.sbt @@ -1,4 +1,4 @@ val checkReloaded = taskKey[Unit]("Asserts that the build was reloaded") checkReloaded := { () } -watchOnIteration := { _ => Watched.CancelWatch } +watchOnIteration := { _ => Watch.CancelWatch } diff --git a/sbt/src/sbt-test/watch/on-start-watch/extra.sbt b/sbt/src/sbt-test/watch/on-start-watch/extra.sbt index 6af4f2331..d6ee90d91 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/extra.sbt +++ b/sbt/src/sbt-test/watch/on-start-watch/extra.sbt @@ -1 +1 @@ -watchOnStart := { _ => () => Watched.Reload } +watchOnStart := { _ => () => Watch.Reload } diff --git a/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala b/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala index db2258f5c..67d3bf940 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala +++ b/sbt/src/sbt-test/watch/on-start-watch/project/Count.scala @@ -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) } diff --git a/sbt/src/sbt-test/watch/on-start-watch/test b/sbt/src/sbt-test/watch/on-start-watch/test index f550e66b6..a917df61d 100644 --- a/sbt/src/sbt-test/watch/on-start-watch/test +++ b/sbt/src/sbt-test/watch/on-start-watch/test @@ -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 diff --git a/sbt/src/sbt-test/watch/task/changes/Build.scala b/sbt/src/sbt-test/watch/task/changes/Build.scala index 1c5162d37..abb5f37b9 100644 --- a/sbt/src/sbt-test/watch/task/changes/Build.scala +++ b/sbt/src/sbt-test/watch/task/changes/Build.scala @@ -22,6 +22,6 @@ object Build { IO.touch(baseDirectory.value / "foo.txt", true) Some("watching") }, - watchOnStart := { _ => () => Watched.CancelWatch } + watchOnStart := { _ => () => Watch.CancelWatch } ) -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/watch/task/project/Build.scala b/sbt/src/sbt-test/watch/task/project/Build.scala index f0beda1c1..f1a6adbd9 100644 --- a/sbt/src/sbt-test/watch/task/project/Build.scala +++ b/sbt/src/sbt-test/watch/task/project/Build.scala @@ -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 } } ) diff --git a/sbt/src/sbt-test/watch/task/test b/sbt/src/sbt-test/watch/task/test index b3e02de19..93022959f 100644 --- a/sbt/src/sbt-test/watch/task/test +++ b/sbt/src/sbt-test/watch/task/test @@ -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