Merge pull request #4867 from eatkins/watch-trigger

Watch trigger
This commit is contained in:
eugene yokota 2019-07-15 21:40:28 -04:00 committed by GitHub
commit d4df289f2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 107 deletions

View File

@ -8,7 +8,8 @@
package sbt
package internal
import java.io.{ ByteArrayInputStream, InputStream, File => _ }
import java.io.{ ByteArrayInputStream, IOException, InputStream, File => _ }
import java.nio.file.Path
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import sbt.BasicCommandStrings.{
@ -26,10 +27,9 @@ import sbt.internal.nio._
import sbt.internal.util.complete.Parser._
import sbt.internal.util.complete.{ Parser, Parsers }
import sbt.internal.util.{ AttributeKey, JLine, Util }
import sbt.nio.FileStamper.LastModified
import sbt.nio.Keys.{ fileInputs, _ }
import sbt.nio.Watch.{ Creation, Deletion, ShowOptions, Update }
import sbt.nio.file.FileAttributes
import sbt.nio.file.{ FileAttributes, Glob }
import sbt.nio.{ FileStamp, FileStamper, Watch }
import sbt.util.{ Level, _ }
@ -329,12 +329,14 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
val fileStampCache = new FileStamp.Cache
repo.addObserver(t => fileStampCache.invalidate(t.path))
val persistFileStamps = extracted.get(watchPersistFileStamps)
val cachingRepo: FileTreeRepository[FileAttributes] =
if (persistFileStamps) repo else new FileStampRepository(fileStampCache, repo)
try {
val stateWithRepo = state.put(globalFileTreeRepository, repo)
val stateWithRepo = state.put(globalFileTreeRepository, cachingRepo)
val fullState =
addLegacyWatchSetting(
if (extracted.get(watchPersistFileStamps))
stateWithRepo.put(persistentFileStampCache, fileStampCache)
if (persistFileStamps) stateWithRepo.put(persistentFileStampCache, fileStampCache)
else stateWithRepo
)
setup(fullState, commands) { (s, valid, invalid) =>
@ -579,10 +581,66 @@ private[sbt] object Continuous extends DeprecatedContinuous {
val retentionPeriod = configs.map(_.watchSettings.antiEntropyRetentionPeriod).max
val quarantinePeriod = configs.map(_.watchSettings.deletionQuarantinePeriod).max
val monitor: FileEventMonitor[Event] = new FileEventMonitor[Event] {
private implicit class WatchLogger(val l: Logger) extends sbt.internal.nio.WatchLogger {
override def debug(msg: Any): Unit = l.debug(msg.toString)
}
private[this] val observers: Observers[Event] = new Observers
private[this] val repo = getRepository(state)
private[this] val handle = repo.addObserver(observers)
private[this] val eventMonitorObservers = new Observers[Event]
private[this] val configHandle: AutoCloseable =
observers.addObserver { e =>
// We only want to create one event per actual source file event. It doesn't matter
// which of the config inputs triggers the event because they all will be used in
// the onEvent callback above.
configs.find(_.inputs().exists(_.glob.matches(e.path))) match {
case Some(config) =>
val configLogger = logger.withPrefix(config.command)
configLogger.debug(s"Accepted event for ${e.path}")
eventMonitorObservers.onNext(e)
case None =>
}
if (trackMetaBuild && buildGlobs.exists(_.matches(e.path))) {
val metaLogger = logger.withPrefix("build")
metaLogger.debug(s"Accepted event for ${e.path}")
eventMonitorObservers.onNext(e)
}
}
if (trackMetaBuild) buildGlobs.foreach(repo.register)
private[this] val monitor = FileEventMonitor.antiEntropy(
eventMonitorObservers,
configs.map(_.watchSettings.antiEntropy).max,
logger,
quarantinePeriod,
retentionPeriod
)
override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] =
monitor.poll(duration, filter)
override def close(): Unit = {
configHandle.close()
handle.close()
}
}
val watchLogger: WatchLogger = msg => logger.debug(msg.toString)
val antiEntropy = configs.map(_.watchSettings.antiEntropy).max
val antiEntropyMonitor = FileEventMonitor.antiEntropy(
monitor,
antiEntropy,
watchLogger,
quarantinePeriod,
retentionPeriod
)
val onEvent: Event => Seq[(Watch.Event, Watch.Action)] = event => {
val path = event.path
def watchEvent(forceTrigger: Boolean): Option[Watch.Event] = {
def getWatchEvent(forceTrigger: Boolean): Option[Watch.Event] = {
if (!event.exists) {
Some(Deletion(event))
fileStampCache.remove(event.path) match {
@ -614,102 +672,35 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
if (buildGlobs.exists(_.matches(path))) {
watchEvent(forceTrigger = false).map(e => e -> Watch.Reload).toSeq
getWatchEvent(forceTrigger = false).map(e => e -> Watch.Reload).toSeq
} else {
configs
.flatMap { config =>
config
.inputs()
.filter(_.glob.matches(path))
.sortBy(_.fileStamper match {
case FileStamper.Hash => -1
case FileStamper.LastModified => 0
})
.headOption
.flatMap { d =>
val forceTrigger = d.forceTrigger
d.fileStamper match {
// We do not update the file stamp cache because we only want hashes in that
// cache or else it can mess up the external hooks that use that cache.
case LastModified =>
logger.debug(s"Trigger path detected $path")
val watchEvent =
if (!event.exists) Deletion(event)
else if (fileStampCache.get(path).isDefined) Creation(event)
else Update(event)
val action = config.watchSettings.onFileInputEvent(count.get(), watchEvent)
Some(watchEvent -> action)
case _ =>
watchEvent(forceTrigger).flatMap { e =>
val action = config.watchSettings.onFileInputEvent(count.get(), e)
if (action != Watch.Ignore) Some(e -> action) else None
}
}
}
} match {
case events if events.isEmpty => Nil
case events => events.minBy(_._2) :: Nil
val acceptedConfigParameters = configs.flatMap { config =>
config.inputs().flatMap {
case i if i.glob.matches(path) =>
Some((i.forceTrigger, i.fileStamper, config.watchSettings.onFileInputEvent))
case _ => None
}
}
}
}
val monitor: FileEventMonitor[Event] = new FileEventMonitor[Event] {
private implicit class WatchLogger(val l: Logger) extends sbt.internal.nio.WatchLogger {
override def debug(msg: Any): Unit = l.debug(msg.toString)
}
private[this] val observers: Observers[Event] = new Observers
private[this] val repo = getRepository(state)
private[this] val handle = repo.addObserver(observers)
private[this] val eventMonitorObservers = new Observers[Event]
private[this] val delegateHandles: Seq[AutoCloseable] =
configs.map { config =>
// Create a logger with a scoped key prefix so that we can tell from which task there
// were inputs that matched the event path.
val configLogger = logger.withPrefix(config.command)
observers.addObserver { e =>
if (config.inputs().exists(_.glob.matches(e.path))) {
configLogger.debug(s"Accepted event for ${e.path}")
eventMonitorObservers.onNext(e)
if (acceptedConfigParameters.nonEmpty) {
val useHash = acceptedConfigParameters.exists(_._2 == FileStamper.Hash)
val forceTrigger = acceptedConfigParameters.exists(_._1)
val watchEvent =
if (useHash) getWatchEvent(forceTrigger)
else {
logger.debug(s"Trigger path detected $path")
Some(
if (!event.exists) Deletion(event)
else if (fileStampCache.get(path).isDefined) Creation(event)
else Update(event)
)
}
acceptedConfigParameters.flatMap {
case (_, _, callback) =>
watchEvent.map(e => e -> callback(count.get(), e))
}
}
if (trackMetaBuild) {
buildGlobs.foreach(repo.register)
val metaLogger = logger.withPrefix("meta-build")
observers.addObserver { e =>
if (buildGlobs.exists(_.matches(e.path))) {
metaLogger.debug(s"Accepted event for ${e.path}")
eventMonitorObservers.onNext(e)
}
}
}
private[this] val monitor = FileEventMonitor.antiEntropy(
eventMonitorObservers,
configs.map(_.watchSettings.antiEntropy).max,
logger,
quarantinePeriod,
retentionPeriod
)
override def poll(duration: Duration, filter: Event => Boolean): Seq[Event] = {
monitor.poll(duration, filter)
}
override def close(): Unit = {
delegateHandles.foreach(_.close())
handle.close()
} else Nil
}
}
val watchLogger: WatchLogger = msg => logger.debug(msg.toString)
val antiEntropy = configs.map(_.watchSettings.antiEntropy).max
val antiEntropyMonitor = FileEventMonitor.antiEntropy(
monitor,
antiEntropy,
watchLogger,
quarantinePeriod,
retentionPeriod
)
/*
* This is a callback that will be invoked whenever onEvent returns a Trigger action. The
* motivation is to allow the user to specify this callback via setting so that, for example,
@ -728,7 +719,8 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
(() => {
val actions = antiEntropyMonitor.poll(30.milliseconds).flatMap(onEvent)
val events = antiEntropyMonitor.poll(30.milliseconds)
val actions = events.flatMap(onEvent)
if (actions.exists(_._2 != Watch.Ignore)) {
val builder = new StringBuilder
val min = actions.minBy {
@ -1102,4 +1094,17 @@ private[sbt] object Continuous extends DeprecatedContinuous {
}
}
private[sbt] class FileStampRepository(
fileStampCache: FileStamp.Cache,
underlying: FileTreeRepository[FileAttributes]
) extends FileTreeRepository[FileAttributes] {
def putIfAbsent(path: Path, stamper: FileStamper): (Option[FileStamp], Option[FileStamp]) =
fileStampCache.putIfAbsent(path, stamper)
override def list(path: Path): Seq[(Path, FileAttributes)] = underlying.list(path)
override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable =
underlying.addObserver(observer)
override def register(glob: Glob): Either[IOException, Observable[FileEvent[FileAttributes]]] =
underlying.register(glob)
override def close(): Unit = underlying.close()
}
}

View File

@ -253,12 +253,12 @@ private[sbt] object FileStamp {
case e => e.value
}
def putIfAbsent(key: Path, stamper: FileStamper): Unit = {
def putIfAbsent(key: Path, stamper: FileStamper): (Option[FileStamp], Option[FileStamp]) = {
underlying.get(key) match {
case null => updateImpl(key, stamper)
case _ =>
case null => (None, updateImpl(key, stamper))
case Right(s) => (Some(s), None)
case Left(_) => (None, None)
}
()
}
def update(key: Path, stamper: FileStamper): (Option[FileStamp], Option[FileStamp]) = {
underlying.get(key) match {

View File

@ -13,6 +13,7 @@ import java.nio.file.{ Files, Path }
import sbt.Project._
import sbt.internal.Clean.ToSeqPath
import sbt.internal.Continuous.FileStampRepository
import sbt.internal.util.{ AttributeKey, SourcePosition }
import sbt.internal.{ Clean, Continuous, DynamicInput, SettingsGraph }
import sbt.nio.FileStamp.{ fileStampJsonFormatter, pathJsonFormatter, _ }
@ -319,10 +320,22 @@ private[sbt] object Settings {
addTaskDefinition(Keys.inputFileStamps in scopedKey.scope := {
val cache = (unmanagedFileStampCache in scopedKey.scope).value
val stamper = (Keys.inputFileStamper in scopedKey.scope).value
val stampFile: Path => Option[(Path, FileStamp)] =
sbt.Keys.state.value.get(globalFileTreeRepository) match {
case Some(repo: FileStampRepository) =>
(path: Path) =>
repo.putIfAbsent(path, stamper) match {
case (None, Some(s)) =>
cache.put(path, s)
Some(path -> s)
case _ => cache.getOrElseUpdate(path, stamper).map(path -> _)
}
case _ =>
(path: Path) => cache.getOrElseUpdate(path, stamper).map(path -> _)
}
(Keys.allInputPathsAndAttributes in scopedKey.scope).value.flatMap {
case (p, a) if a.isRegularFile && !Files.isHidden(p) =>
cache.getOrElseUpdate(p, stamper).map(p -> _)
case _ => None
case (path, a) if a.isRegularFile && !Files.isHidden(path) => stampFile(path)
case _ => None
}
})
private[this] def outputsAndStamps[T: JsonFormat: ToSeqPath](

View File

@ -576,7 +576,7 @@ object Watch {
sbt.Keys.aggregate in watchTasks :== false,
watchTriggeredMessage :== Watch.defaultOnTriggerMessage,
watchForceTriggerOnAnyChange :== false,
watchPersistFileStamps :== true,
watchPersistFileStamps := (sbt.Keys.turbo in ThisBuild).value,
watchTriggers :== Nil,
)
}

View File

@ -33,4 +33,4 @@ watchOnFileInputEvent := { (_, event: Watch.Event) =>
else Watch.Trigger
}
watchAntiEntropy := 0.millis
watchAntiEntropy := 0.millis