All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sbt.internal.nio.WatchServiceBackedObservable.scala Maven / Gradle / Ivy

/*
 * sbt IO
 * Copyright Scala Center, Lightbend, and Mark Harrah
 *
 * Licensed under Apache License 2.0
 * SPDX-License-Identifier: Apache-2.0
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package sbt.internal.nio

import java.io.IOException
import java.nio.file.StandardWatchEventKinds.OVERFLOW
import java.nio.file.{ Path, WatchKey }
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
import java.util.concurrent.{ CountDownLatch, TimeUnit }

import sbt.internal.io._
import sbt.internal.nio.FileEvent.{ Creation, Deletion }
import sbt.nio.file.FileAttributes.NonExistent
import sbt.nio.file.Glob._
import sbt.nio.file.{ AnyPath, FileAttributes, FileTreeView, Glob, RecursiveGlob }

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.concurrent.duration._
import scala.util.control.NonFatal

private[sbt] object WatchServiceBackedObservable {
  private val eventThreadId = new AtomicInteger(0)
}
private[sbt] class WatchServiceBackedObservable(
    s: NewWatchState,
    delay: FiniteDuration,
    closeService: Boolean,
    logger: WatchLogger
) extends Registerable[FileEvent[FileAttributes]]
    with Observable[FileEvent[FileAttributes]] {
  import WatchServiceBackedObservable.eventThreadId
  private[this] type Event = FileEvent[FileAttributes]
  private[this] val closed = new AtomicBoolean(false)
  private[this] val observers = new Observers[FileEvent[FileAttributes]]
  private[this] val fileCache = new FileCache(p => FileAttributes(p).getOrElse(NonExistent))
  private[this] val view: FileTreeView.Nio[FileAttributes] = FileTreeView.default
  private[this] val thread: Thread = {
    val latch = new CountDownLatch(1)
    new Thread(s"watch-state-event-thread-${eventThreadId.incrementAndGet()}") {
      setDaemon(true)
      start()
      if (!latch.await(5, TimeUnit.SECONDS))
        throw new IllegalStateException("Couldn't start event thread")
      @tailrec
      final def loopImpl(): Unit = {
        try {
          if (!closed.get) getFilesForKey(s.service.poll(delay)).foreach { event =>
            observers.onNext(event)
          }
        } catch {
          case NonFatal(e) =>
            logger.debug(
              s"Error getting files from ${s.service}: $e\n${e.getStackTrace mkString "\n"}"
            )
        }
        if (!closed.get) loopImpl()
      }
      override def run(): Unit = {
        latch.countDown()
        try {
          loopImpl()
        } catch {
          case _: InterruptedException =>
        }
      }

      def getFilesForKey(key: WatchKey): Seq[Event] = key match {
        case null => Vector.empty
        case k =>
          val rawEvents = k.synchronized {
            val events = k.pollEvents.asScala.toVector
            k.reset()
            events
          }
          val keyPath = k.watchable.asInstanceOf[Path]
          val allEvents: Seq[Event] = rawEvents.flatMap {
            case e if e.kind.equals(OVERFLOW) =>
              fileCache.refresh(Glob(keyPath, RecursiveGlob))
            case e if !e.kind.equals(OVERFLOW) && e.context != null =>
              val path = keyPath.resolve(e.context.asInstanceOf[Path])
              FileAttributes(path) match {
                case Right(attrs) => fileCache.update(path, attrs)
                case _            => Nil
              }
            case _ => Nil: Seq[Event]
          }
          logger.debug(s"Received events:\n${allEvents.mkString("\n")}")
          def process(event: Event): Seq[Event] = {
            (event match {
              case Creation(path, attrs) if attrs.isDirectory =>
                s.register(path)
                event +: view.list(Glob(path, RecursiveGlob)).flatMap { case (p, a) =>
                  process(Creation(p, a))
                }
              case Deletion(p, attrs) if attrs.isDirectory =>
                val events = fileCache.refresh(Glob(p, RecursiveGlob))
                events.view.filter(_.attributes.isDirectory).foreach(e => s.unregister(e.path))
                events
              case e => e :: Nil
            }).map {
              case d @ Deletion(path, attributes) =>
                val newAttrs = FileAttributes(
                  isDirectory = attributes.isDirectory,
                  isOther = false,
                  isRegularFile = attributes.isRegularFile,
                  isSymbolicLink = attributes.isSymbolicLink
                )
                Deletion(path, newAttrs, d.occurredAt)
              case e => e
            }
          }
          allEvents.flatMap(process)
      }

    }
  }
  override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable =
    observers.addObserver(observer)

  override def close(): Unit = {
    if (closed.compareAndSet(false, true)) {
      thread.interrupt()
      thread.join(5.seconds.toMillis)
      if (closeService) s.service.close()
      logger.debug("Closed WatchServiceBackedObservable")
    }
  }

  override def register(glob: Glob): Either[IOException, Observable[FileEvent[FileAttributes]]] = {
    try {
      fileCache.register(glob)
      val updatedGlob = glob.range._2 match {
        case Int.MaxValue => Glob(glob.base, RecursiveGlob)
        case d            => (1 to d).foldLeft(Glob(glob.base)) { case (g, _) => g / AnyPath }
      }
      fileCache.list(updatedGlob) foreach {
        case (path, attrs) if attrs.isDirectory => s.register(path)
        case _                                  =>
      }
      val observable = new Observers[FileEvent[FileAttributes]] {
        override def onNext(t: FileEvent[FileAttributes]): Unit = {
          if (glob.matches(t.path)) super.onNext(t)
        }
      }
      val handle = observers.addObserver(observable)
      Right(new Observable[FileEvent[FileAttributes]] {
        override def addObserver(observer: Observer[FileEvent[FileAttributes]]): AutoCloseable =
          observable.addObserver(observer)
        override def close(): Unit = handle.close()
        override def toString = s"Observable($glob)"
      })
    } catch {
      case e: IOException => Left(e)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy