This new version of io breaks source and binary compatibility everywhere
that uses the register(path: Path, depth: Int) method that is defined on
a few interfaces because I changed the signature to register(glob:
Glob). I had to convert to using a glob everywhere that register was
called.

I also noticed a number of places where we were calling .asFile on a
file. This is redundant because asFile is an extension method on File
that just returns the underlying file.

Finally, I share the IOSyntax trait from io in AllSyntax. There was more
or less a TODO suggesting this change. The one hairy part is the
existence of the Alternative class. This class has unfortunately somehow
made it into the sbt package object. While I doubt many plugins are
using this, it doesn't seem worth breaking binary compatibility to get
rid of it. The issue is that while Alternative is defined private[sbt],
the alternative method in IOSyntax is public, so I can't get rid of
Alternative without breaking binary compatibility.

I'm not deprecating Alternative for now because the sbtProj still has
xfatal warnings on. I think in many, if not most, cases, the Alternative
class makes the code more confusing as is often the case with custom
operators. The confusion is mitigated if the abstraction is used only in
the file in which it's defined.
This commit is contained in:
Ethan Atkins
2019-03-22 07:53:41 -07:00
parent ffa69ea5d6
commit f7f7addff7
11 changed files with 122 additions and 97 deletions
@@ -27,10 +27,8 @@ final class FileTreeViewConfig private (
) => FileEventMonitor[FileCacheEntry]
)
object FileTreeViewConfig {
private implicit class RepositoryOps(val repository: FileTreeRepository[FileCacheEntry]) {
def register(sources: Seq[WatchSource]): Unit = sources foreach { s =>
repository.register(s.base.toPath, if (s.recursive) Integer.MAX_VALUE else 0)
}
private implicit class SourceOps(val s: WatchSource) extends AnyVal {
def toGlob: Glob = Glob(s.base, AllPassFilter, if (s.recursive) Integer.MAX_VALUE else 0)
}
/**
@@ -76,14 +74,16 @@ object FileTreeViewConfig {
val ioLogger: sbt.io.WatchLogger = msg => logger.debug(msg.toString)
FileEventMonitor.antiEntropy(
new WatchServiceBackedObservable(
WatchState.empty(Watched.createWatchService(), sources),
WatchState.empty(sources.map(_.toGlob), Watched.createWatchService()),
delay,
FileCacheEntry.default,
closeService = true,
ioLogger
),
antiEntropy,
ioLogger
ioLogger,
50.milliseconds,
10.seconds
)
}
)
@@ -104,14 +104,20 @@ object FileTreeViewConfig {
sources: Seq[WatchSource],
logger: Logger
) => {
repository.register(sources)
sources.view.map(_.toGlob).foreach(repository.register)
val copied = new Observable[FileCacheEntry] {
override def addObserver(observer: Observer[FileCacheEntry]): Int =
repository.addObserver(observer)
override def removeObserver(handle: Int): Unit = repository.removeObserver(handle)
override def close(): Unit = {} // Don't close the underlying observable
}
FileEventMonitor.antiEntropy(copied, antiEntropy, msg => logger.debug(msg.toString))
FileEventMonitor.antiEntropy(
copied,
antiEntropy,
msg => logger.debug(msg.toString),
50.milliseconds,
10.seconds
)
}
)
@@ -159,18 +165,20 @@ object FileTreeViewConfig {
pollingInterval: FiniteDuration,
pollingSources: Seq[WatchSource],
): FileTreeViewConfig = FileTreeViewConfig(
() => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources: _*),
() => FileTreeRepository.hybrid(FileCacheEntry.default, pollingSources.map(_.toGlob): _*),
(
repository: HybridPollingFileTreeRepository[FileCacheEntry],
sources: Seq[WatchSource],
logger: Logger
) => {
repository.register(sources)
sources.view.map(_.toGlob).foreach(repository.register)
FileEventMonitor
.antiEntropy(
repository.toPollingObservable(pollingInterval, sources, NullWatchLogger),
repository.toPollingRepository(pollingInterval, NullWatchLogger),
antiEntropy,
msg => logger.debug(msg.toString)
msg => logger.debug(msg.toString),
50.milliseconds,
10.seconds
)
}
)
@@ -16,7 +16,8 @@ import sbt.Watched._
import sbt.WatchedSpec._
import sbt.internal.FileCacheEntry
import sbt.io.FileEventMonitor.Event
import sbt.io.{ FileEventMonitor, IO, TypedPath }
import sbt.io._
import sbt.io.syntax._
import sbt.util.Logger
import scala.collection.mutable
@@ -26,7 +27,7 @@ class WatchedSpec extends FlatSpec with Matchers {
object Defaults {
private val fileTreeViewConfig = FileTreeViewConfig.default(50.millis)
def config(
sources: Seq[WatchSource],
globs: Seq[Glob],
fileEventMonitor: Option[FileEventMonitor[FileCacheEntry]] = None,
logger: Logger = NullLogger,
handleInput: InputStream => Action = _ => Ignore,
@@ -35,9 +36,17 @@ class WatchedSpec extends FlatSpec with Matchers {
triggeredMessage: (TypedPath, Int) => Option[String] = (_, _) => None,
watchingMessage: Int => Option[String] = _ => None
): WatchConfig = {
val monitor = fileEventMonitor.getOrElse(
fileTreeViewConfig.newMonitor(fileTreeViewConfig.newDataView(), sources, logger)
)
val monitor = fileEventMonitor.getOrElse {
val fileTreeRepository = FileTreeRepository.default(FileCacheEntry.default)
globs.foreach(fileTreeRepository.register)
FileEventMonitor.antiEntropy(
fileTreeRepository,
50.millis,
m => logger.debug(m.toString),
50.milliseconds,
100.milliseconds
)
}
WatchConfig.default(
logger = logger,
monitor,
@@ -55,13 +64,13 @@ class WatchedSpec extends FlatSpec with Matchers {
override def read(): Int = -1
}
"Watched.watch" should "stop" in IO.withTemporaryDirectory { dir =>
val config = Defaults.config(sources = Seq(WatchSource(dir.toRealPath)))
val config = Defaults.config(globs = Seq(dir.toRealPath.toGlob))
Watched.watch(NullInputStream, () => Right(true), config) shouldBe CancelWatch
}
it should "trigger" in IO.withTemporaryDirectory { dir =>
val triggered = new AtomicBoolean(false)
val config = Defaults.config(
sources = Seq(WatchSource(dir.toRealPath)),
globs = Seq(dir.toRealPath ** AllPassFilter),
preWatch = (count, _) => if (count == 2) CancelWatch else Ignore,
onWatchEvent = _ => { triggered.set(true); Trigger },
watchingMessage = _ => {
@@ -77,7 +86,7 @@ class WatchedSpec extends FlatSpec with Matchers {
val foo = realDir.toPath.resolve("foo")
val bar = realDir.toPath.resolve("bar")
val config = Defaults.config(
sources = Seq(WatchSource(realDir)),
globs = Seq(realDir ** AllPassFilter),
preWatch = (count, _) => if (count == 2) CancelWatch else Ignore,
onWatchEvent = e => if (e.entry.typedPath.toPath == foo) Trigger else Ignore,
triggeredMessage = (tp, _) => { queue += tp; None },
@@ -92,7 +101,7 @@ class WatchedSpec extends FlatSpec with Matchers {
val foo = realDir.toPath.resolve("foo")
val bar = realDir.toPath.resolve("bar")
val config = Defaults.config(
sources = Seq(WatchSource(realDir)),
globs = Seq(realDir ** AllPassFilter),
preWatch = (count, _) => if (count == 3) CancelWatch else Ignore,
onWatchEvent = _ => Trigger,
triggeredMessage = (tp, _) => { queue += tp; None },
@@ -113,7 +122,7 @@ class WatchedSpec extends FlatSpec with Matchers {
it should "halt on error" in IO.withTemporaryDirectory { dir =>
val halted = new AtomicBoolean(false)
val config = Defaults.config(
sources = Seq(WatchSource(dir.toRealPath)),
globs = Seq(dir.toRealPath ** AllPassFilter),
preWatch = (_, lastStatus) => if (lastStatus) Ignore else { halted.set(true); HandleError }
)
Watched.watch(NullInputStream, () => Right(false), config) shouldBe HandleError
@@ -121,7 +130,7 @@ class WatchedSpec extends FlatSpec with Matchers {
}
it should "reload" in IO.withTemporaryDirectory { dir =>
val config = Defaults.config(
sources = Seq(WatchSource(dir.toRealPath)),
globs = Seq(dir.toRealPath ** AllPassFilter),
preWatch = (_, _) => Ignore,
onWatchEvent = _ => Reload,
watchingMessage = _ => { new File(dir, "file").createNewFile(); None }