diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index 50dcd1cdb52..73cae2ab641 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -6,9 +6,9 @@ package sbt import BasicCommandStrings.ClearOnFailure import State.FailureWall import annotation.tailrec -import java.io.File +import java.nio.file.FileSystems -import sbt.io.PathFinder +import sbt.io.WatchService import sbt.internal.io.{ SourceModificationWatch, WatchState } import sbt.internal.util.AttributeKey import sbt.internal.util.Types.const @@ -16,20 +16,23 @@ import sbt.internal.util.Types.const trait Watched { /** The files watched when an action is run with a preceeding ~ */ - def watchPaths(s: State): Seq[File] = Nil + def watchSources(s: State): Seq[WatchState.Source] = Nil def terminateWatch(key: Int): Boolean = Watched.isEnter(key) /** * The time in milliseconds between checking for changes. The actual time between the last change made to a file and the * execution time is between `pollInterval` and `pollInterval*2`. */ - def pollInterval: Int = Watched.PollDelayMillis + def pollInterval: Long = Watched.PollDelayMillis /** The message to show when triggered execution waits for sources to change.*/ private[sbt] def watchingMessage(s: WatchState): String = Watched.defaultWatchingMessage(s) /** The message to show before an action is run. */ private[sbt] def triggeredMessage(s: WatchState): String = Watched.defaultTriggeredMessage(s) + + /** The `WatchService` to use to monitor the file system. */ + private[sbt] def watchService(): WatchService = FileSystems.getDefault.newWatchService() } object Watched { @@ -43,7 +46,7 @@ object Watched { def multi(base: Watched, paths: Seq[Watched]): Watched = new AWatched { - override def watchPaths(s: State) = (base.watchPaths(s) /: paths)(_ ++ _.watchPaths(s)) + override def watchSources(s: State) = (base.watchSources(s) /: paths)(_ ++ _.watchSources(s)) override def terminateWatch(key: Int): Boolean = base.terminateWatch(key) override val pollInterval = (base +: paths).map(_.pollInterval).min override def watchingMessage(s: WatchState) = base.watchingMessage(s) @@ -51,15 +54,16 @@ object Watched { } def empty: Watched = new AWatched - val PollDelayMillis = 500 + val PollDelayMillis: Long = 500 def isEnter(key: Int): Boolean = key == 10 || key == 13 def printIfDefined(msg: String) = if (!msg.isEmpty) System.out.println(msg) def executeContinuously(watched: Watched, s: State, next: String, repeat: String): State = { @tailrec def shouldTerminate: Boolean = (System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate) - val sourcesFinder = PathFinder { watched watchPaths s } - val watchState = s get ContinuousState getOrElse WatchState.empty + val sources = watched.watchSources(s) + val service = watched.watchService() + val watchState = s get ContinuousState getOrElse WatchState.empty(service, sources) if (watchState.count > 0) printIfDefined(watched watchingMessage watchState) @@ -67,8 +71,7 @@ object Watched { val (triggered, newWatchState) = try { val (triggered, newWatchState) = - SourceModificationWatch.watch(sourcesFinder, watched.pollInterval, watchState)( - shouldTerminate) + SourceModificationWatch.watch(watched.pollInterval, watchState)(shouldTerminate) (triggered, newWatchState) } catch { case e: Exception => @@ -84,7 +87,8 @@ object Watched { (ClearOnFailure :: next :: FailureWall :: repeat :: s).put(ContinuousState, newWatchState) } else { while (System.in.available() > 0) System.in.read() - s.put(ContinuousState, WatchState.empty) + service.close() + s.remove(ContinuousState) } } val ContinuousState = diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index c9e2d297b0c..08ce4044785 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -5,6 +5,7 @@ package sbt import Def.{ Initialize, ScopedKey, Setting, SettingsDefinition } import java.io.{ File, PrintWriter } +import java.nio.file.FileSystems import java.net.{ URI, URL } import java.util.Optional import java.util.concurrent.{ TimeUnit, Callable } @@ -47,7 +48,8 @@ import sbt.io.{ PathFinder, SimpleFileFilter, DirectoryFilter, - Hash + Hash, + WatchService }, Path._ import sbt.librarymanagement.Artifact.{ DocClassifier, SourceClassifier } import sbt.librarymanagement.Configurations.{ @@ -250,7 +252,10 @@ object Defaults extends BuildCommon { Previous.references :== new Previous.References, concurrentRestrictions := defaultRestrictions.value, parallelExecution :== true, - pollInterval :== 500, + pollInterval :== 500L, + watchService :== { () => + FileSystems.getDefault.newWatchService + }, logBuffered :== false, commands :== Nil, showSuccess :== true, @@ -314,7 +319,12 @@ object Defaults extends BuildCommon { unmanagedSources := collectFiles(unmanagedSourceDirectories, includeFilter in unmanagedSources, excludeFilter in unmanagedSources).value, - watchSources in ConfigGlobal ++= unmanagedSources.value, + watchSources in ConfigGlobal ++= { + val bases = unmanagedSourceDirectories.value + val include = (includeFilter in unmanagedSources).value + val exclude = (excludeFilter in unmanagedSources).value + bases.map(b => (b, include, exclude)) + }, managedSourceDirectories := Seq(sourceManaged.value), managedSources := generate(sourceGenerators).value, sourceGenerators :== Nil, @@ -334,7 +344,12 @@ object Defaults extends BuildCommon { unmanagedResources := collectFiles(unmanagedResourceDirectories, includeFilter in unmanagedResources, excludeFilter in unmanagedResources).value, - watchSources in ConfigGlobal ++= unmanagedResources.value, + watchSources in ConfigGlobal ++= { + val bases = unmanagedResourceDirectories.value + val include = (includeFilter in unmanagedResources).value + val exclude = (excludeFilter in unmanagedResources).value + bases.map(b => (b, include, exclude)) + }, resourceGenerators :== Nil, resourceGenerators += Def.task { PluginDiscovery.writeDescriptors(discoveredSbtPlugins.value, resourceManaged.value) @@ -536,7 +551,7 @@ object Defaults extends BuildCommon { def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] = generators { _.join.map(_.flatten) } - def watchTransitiveSourcesTask: Initialize[Task[Seq[File]]] = { + def watchTransitiveSourcesTask: Initialize[Task[Seq[WatchState.Source]]] = { import ScopeFilter.Make.{ inDependencies => inDeps, _ } val selectDeps = ScopeFilter(inAggregates(ThisProject) || inDeps(ThisProject)) val allWatched = (watchSources ?? Nil).all(selectDeps) @@ -553,6 +568,7 @@ object Defaults extends BuildCommon { def watchSetting: Initialize[Watched] = Def.setting { + val getService = watchService.value val interval = pollInterval.value val base = thisProjectRef.value val msg = watchingMessage.value @@ -563,11 +579,13 @@ object Defaults extends BuildCommon { override def pollInterval = interval override def watchingMessage(s: WatchState) = msg(s) override def triggeredMessage(s: WatchState) = trigMsg(s) - override def watchPaths(s: State) = EvaluateTask(Project structure s, key, s, base) match { - case Some((_, Value(ps))) => ps - case Some((_, Inc(i))) => throw i - case None => sys.error("key not found: " + Def.displayFull(key)) - } + override def watchService() = getService() + override def watchSources(s: State) = + EvaluateTask(Project structure s, key, s, base) match { + case Some((_, Value(ps))) => ps + case Some((_, Inc(i))) => throw i + case None => sys.error("key not found: " + Def.displayFull(key)) + } } } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index a0575580a4b..fc04506edb4 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -36,7 +36,7 @@ import sbt.internal.{ SessionSettings, LogManager } -import sbt.io.FileFilter +import sbt.io.{ FileFilter, WatchService } import sbt.internal.io.WatchState import sbt.internal.util.{ AttributeKey, SourcePosition } @@ -130,9 +130,10 @@ object Keys { val analysis = AttributeKey[CompileAnalysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.", DSetting) val watch = SettingKey(BasicKeys.watch) val suppressSbtShellNotification = SettingKey[Boolean]("suppressSbtShellNotification", """True to suppress the "Executing in batch mode.." message.""", CSetting) - val pollInterval = SettingKey[Int]("poll-interval", "Interval between checks for modified sources by the continuous execution command.", BMinusSetting) - val watchSources = TaskKey[Seq[File]]("watch-sources", "Defines the sources in this project for continuous execution to watch for changes.", BMinusSetting) - val watchTransitiveSources = TaskKey[Seq[File]]("watch-transitive-sources", "Defines the sources in all projects for continuous execution to watch.", CSetting) + val pollInterval = SettingKey[Long]("poll-interval", "Interval between checks for modified sources by the continuous execution command.", BMinusSetting) + val watchService = SettingKey[() => WatchService]("watch-service", "Service to use to monitor file system changes.", BMinusSetting) + val watchSources = TaskKey[Seq[WatchState.Source]]("watch-sources", "Defines the sources in this project for continuous execution to watch for changes.", BMinusSetting) + val watchTransitiveSources = TaskKey[Seq[WatchState.Source]]("watch-transitive-sources", "Defines the sources in all projects for continuous execution to watch.", CSetting) val watchingMessage = SettingKey[WatchState => String]("watching-message", "The message to show when triggered execution waits for sources to change.", DSetting) val triggeredMessage = SettingKey[WatchState => String]("triggered-message", "The message to show before triggered execution executes an action after sources change.", DSetting)