package play.runsupport
import java.net.URLClassLoader
import java.util.List
import java.util.Locale
import sbt._
import sbt.Path._
import scala.collection.JavaConversions
import scala.reflect.ClassTag
import scala.util.{ Properties, Try }
import scala.util.control.NonFatal
import java.io.File
import java.util.concurrent.Callable
trait FileWatchService {
def watch(filesToWatch: Seq[File], onChange: () => Unit): FileWatcher
def watch(filesToWatch: List[File], onChange: Callable[Void]): FileWatcher = {
watch(JavaConversions.asScalaBuffer(filesToWatch), () => { onChange.call })
}
}
trait FileWatcher {
def stop(): Unit
}
object FileWatchService {
private sealed trait OS
private case object Windows extends OS
private case object Linux extends OS
private case object OSX extends OS
private case object Other extends OS
private val os: OS = {
sys.props.get("os.name").map { name =>
name.toLowerCase(Locale.ENGLISH) match {
case osx if osx.contains("darwin") || osx.contains("mac") => OSX
case windows if windows.contains("windows") => Windows
case linux if linux.contains("linux") => Linux
case _ => Other
}
}.getOrElse(Other)
}
def defaultWatchService(targetDirectory: File, pollDelayMillis: Int, logger: LoggerProxy): FileWatchService = new FileWatchService {
lazy val delegate = os match {
case (Windows | Linux) if Properties.isJavaAtLeast("1.7") => new JDK7FileWatchService(logger)
case (Windows | Linux | OSX) => JNotifyFileWatchService(targetDirectory).recover {
case e =>
logger.warn("Error loading JNotify watch service: " + e.getMessage)
logger.trace(e)
new PollingFileWatchService(pollDelayMillis)
}.get
case _ => new PollingFileWatchService(pollDelayMillis)
}
def watch(filesToWatch: Seq[File], onChange: () => Unit) = delegate.watch(filesToWatch, onChange)
}
def jnotify(targetDirectory: File): FileWatchService = optional(JNotifyFileWatchService(targetDirectory))
def jdk7(logger: LoggerProxy): FileWatchService = new JDK7FileWatchService(logger)
def sbt(pollDelayMillis: Int): FileWatchService = new PollingFileWatchService(pollDelayMillis)
def optional(watchService: Try[FileWatchService]): FileWatchService = new OptionalFileWatchServiceDelegate(watchService)
}
private[play] trait DefaultFileWatchService extends FileWatchService {
def delegate: FileWatchService
}
private[play] class PollingFileWatchService(val pollDelayMillis: Int) extends FileWatchService {
def distinctPathFinder(pathFinder: PathFinder) = PathFinder {
pathFinder.get.map(p => (p.asFile.getAbsolutePath, p)).toMap.values
}
def watch(filesToWatch: Seq[File], onChange: () => Unit) = {
@volatile var stopped = false
val thread = new Thread(new Runnable {
def run() = {
var state = WatchState.empty
while (!stopped) {
val (triggered, newState) = SourceModificationWatch.watch(distinctPathFinder(filesToWatch.***), pollDelayMillis,
state)(stopped)
if (triggered) onChange()
state = newState
}
}
}, "sbt-play-watch-service")
thread.setDaemon(true)
thread.start()
new FileWatcher {
def stop() = stopped = true
}
}
}
private[play] class JNotifyFileWatchService(delegate: JNotifyFileWatchService.JNotifyDelegate) extends FileWatchService {
def watch(filesToWatch: Seq[File], onChange: () => Unit) = {
val listener = delegate.newListener(onChange)
val registeredIds = filesToWatch.map { file =>
delegate.addWatch(file.getAbsolutePath, listener)
}
new FileWatcher {
def stop() = registeredIds.foreach(delegate.removeWatch)
}
}
}
private object JNotifyFileWatchService {
import java.lang.reflect.{ Method, InvocationHandler, Proxy }
class JNotifyDelegate(classLoader: ClassLoader, listenerClass: Class[_], addWatchMethod: Method, removeWatchMethod: Method) {
def addWatch(fileOrDirectory: String, listener: AnyRef): Int = {
addWatchMethod.invoke(null,
fileOrDirectory,
15: java.lang.Integer,
true: java.lang.Boolean,
listener).asInstanceOf[Int]
}
def removeWatch(id: Int): Unit = {
try {
removeWatchMethod.invoke(null, id.asInstanceOf[AnyRef])
} catch {
case _: Throwable =>
}
}
def newListener(onChange: () => Unit): AnyRef = {
Proxy.newProxyInstance(classLoader, Seq(listenerClass).toArray, new InvocationHandler {
def invoke(proxy: AnyRef, m: Method, args: Array[AnyRef]): AnyRef = {
onChange()
null
}
})
}
@throws[Throwable]("If we were not able to successfully load JNotify")
def ensureLoaded(): Unit = {
removeWatchMethod.invoke(null, 0.asInstanceOf[java.lang.Integer])
}
}
@volatile var watchService: Option[Try[JNotifyFileWatchService]] = None
def apply(targetDirectory: File): Try[FileWatchService] = {
watchService match {
case None =>
val ws = scala.util.control.Exception.allCatch.withTry {
val classloader = GlobalStaticVar.get[ClassLoader]("FileWatchServiceJNotifyHack").getOrElse {
val jnotifyJarFile = this.getClass.getClassLoader.asInstanceOf[java.net.URLClassLoader].getURLs
.map(_.getFile)
.find(_.contains("/jnotify"))
.map(new File(_))
.getOrElse(sys.error("Missing JNotify?"))
val nativeLibrariesDirectory = new File(targetDirectory, "native_libraries")
if (!nativeLibrariesDirectory.exists) {
IO.unzip(jnotifyJarFile, targetDirectory, (name: String) => name.startsWith("native_libraries"))
}
val libs = new File(nativeLibrariesDirectory, System.getProperty("sun.arch.data.model") + "bits").getAbsolutePath
System.setProperty("java.library.path", {
Option(System.getProperty("java.library.path")).map { existing =>
existing + java.io.File.pathSeparator + libs
}.getOrElse(libs)
})
val fieldSysPath = classOf[ClassLoader].getDeclaredField("sys_paths")
fieldSysPath.setAccessible(true)
fieldSysPath.set(null, null)
val loader = new URLClassLoader(Array(jnotifyJarFile.toURI.toURL), null)
GlobalStaticVar.set("FileWatchServiceJNotifyHack", loader)
loader
}
val jnotifyClass = classloader.loadClass("net.contentobjects.jnotify.JNotify")
val jnotifyListenerClass = classloader.loadClass("net.contentobjects.jnotify.JNotifyListener")
val addWatchMethod = jnotifyClass.getMethod("addWatch", classOf[String], classOf[Int], classOf[Boolean], jnotifyListenerClass)
val removeWatchMethod = jnotifyClass.getMethod("removeWatch", classOf[Int])
val d = new JNotifyDelegate(classloader, jnotifyListenerClass, addWatchMethod, removeWatchMethod)
d.ensureLoaded()
new JNotifyFileWatchService(d)
}
watchService = Some(ws)
ws
case Some(ws) => ws
}
}
}
private[play] class JDK7FileWatchService(logger: LoggerProxy) extends FileWatchService {
import java.nio.file._
import StandardWatchEventKinds._
def watch(filesToWatch: Seq[File], onChange: () => Unit) = {
val dirsToWatch = filesToWatch.filter { file =>
if (file.isDirectory) {
true
} else if (file.isFile) {
logger.warn("JDK7 WatchService only supports watching directories, but an attempt has been made to watch the file: " + file.getCanonicalPath)
logger.warn("This file will not be watched. Either remove the file from playMonitoredFiles, or configure a different WatchService, eg:")
logger.warn("PlayKeys.fileWatchService := play.runsupport.FileWatchService.jnotify(target.value)")
false
} else false
}
val watcher = FileSystems.getDefault.newWatchService()
def watchDir(dir: File) = {
dir.toPath.register(watcher, Array[WatchEvent.Kind[_]](ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY),
com.sun.nio.file.SensitivityWatchEventModifier.HIGH)
}
val allDirsToWatch = allSubDirectories(dirsToWatch)
allDirsToWatch.foreach(watchDir)
val thread = new Thread(new Runnable {
def run() = {
try {
while (true) {
val watchKey = watcher.take()
val events = watchKey.pollEvents()
import scala.collection.JavaConversions._
events.foreach { event =>
if (event.kind == ENTRY_CREATE) {
val file = watchKey.watchable.asInstanceOf[Path].resolve(event.context.asInstanceOf[Path]).toFile
if (file.isDirectory) {
allSubDirectories(Seq(file)).foreach(watchDir)
}
}
}
onChange()
watchKey.reset()
}
} catch {
case NonFatal(e) =>
} finally {
watcher.close()
}
}
}, "sbt-play-watch-service")
thread.setDaemon(true)
thread.start()
new FileWatcher {
def stop() = {
watcher.close()
}
}
}
private def allSubDirectories(dirs: Seq[File]) = {
(dirs ** (DirectoryFilter -- HiddenFileFilter)).get.distinct
}
}
private[play] class OptionalFileWatchServiceDelegate(val watchService: Try[FileWatchService]) extends FileWatchService {
def watch(filesToWatch: Seq[File], onChange: () => Unit) = {
watchService.map(ws => ws.watch(filesToWatch, onChange)).get
}
}
private[runsupport] object GlobalStaticVar {
import javax.management._
import javax.management.modelmbean._
import java.lang.management._
import java.util.concurrent.atomic.AtomicReference
private def objectName(name: String) = {
new ObjectName(":type=GlobalStaticVar,name=" + name)
}
def set(name: String, value: AnyRef): Unit = {
val reference = new AtomicReference[AnyRef](value)
val getMethod = classOf[AtomicReference[_]].getMethod("get")
val getInfo = new ModelMBeanOperationInfo("The value", getMethod)
val mmbi = new ModelMBeanInfoSupport("GlobalStaticVar",
"A global static variable",
null,
null,
Array(getInfo),
null);
val mmb = new RequiredModelMBean(mmbi)
mmb.setManagedResource(reference, "ObjectReference")
ManagementFactory.getPlatformMBeanServer.registerMBean(mmb, objectName(name))
}
def get[T](name: String)(implicit ct: ClassTag[T]): Option[T] = {
try {
val value = ManagementFactory.getPlatformMBeanServer.invoke(objectName(name), "get", Array.empty, Array.empty)
if (ct.runtimeClass.isInstance(value)) {
Some(value.asInstanceOf[T])
} else {
throw new ClassCastException(s"Global static var $name is not an instance of ${ct.runtimeClass}, but is actually a ${Option(value).fold("null")(_.getClass.getName)}")
}
} catch {
case e: InstanceNotFoundException =>
None
}
}
def remove(name: String): Unit = {
try {
ManagementFactory.getPlatformMBeanServer.unregisterMBean(objectName(name))
} catch {
case e: InstanceNotFoundException =>
}
}
}