|
|
|
|
@ -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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|