Files
orx/orx-file-watcher/src/main/kotlin/FileWatcher.kt
2020-02-08 13:29:11 +01:00

154 lines
3.6 KiB
Kotlin

package org.operndr.extras.filewatcher
import com.sun.nio.file.SensitivityWatchEventModifier
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.openrndr.Program
import org.openrndr.launch
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchKey
import kotlin.concurrent.thread
class FileWatcher(private val program: Program, val file: File, private val onChange: (File) -> Unit) {
val path = file.absoluteFile.toPath()
val parent = path.parent
val key = pathKeys.getOrPut(parent) {
parent.register(
watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY),
SensitivityWatchEventModifier.HIGH
)
}
val watchers = mutableListOf<() -> Unit>()
init {
watchThread
watching.getOrPut(path) {
mutableListOf()
}.add(this)
keyPaths.getOrPut(key) { parent }
}
fun stop() {
watching[path]?.remove(this)
}
internal fun triggerChange() {
program.launch {
onChange(file)
watchers.forEach { it() }
}
}
}
private val watchers = mutableMapOf<() -> Any, FileWatcher>()
fun <T> watchFile(program: Program, file: File, transducer: (File) -> T): () -> T {
var result = transducer(file)
val watcher = FileWatcher(program, file) {
try {
result = transducer(file)
} catch (e: Throwable) {
e.printStackTrace()
}
}
val function = {
result
}
@Suppress("UNCHECKED_CAST")
watchers[function as () -> Any] = watcher
return function
}
/**
* Stops the watcher
*/
fun <T> (() -> T).stop() {
@Suppress("UNCHECKED_CAST")
watchers[this as () -> Any]?.stop()
}
/**
* Triggers reload
*/
fun <T> (() -> T).triggerChange() {
@Suppress("UNCHECKED_CAST")
watchers[this as () -> Any]?.triggerChange()
}
/**
* add watcher to file watcher
*/
fun <T, R> (() -> T).watch(transducer: (T) -> R):()->R {
var result = transducer(this())
watchers[this as () -> Any?]!!.watchers.add {
result = transducer(this())
}
return { result }
}
@JvmName("programWatchFile")
fun <T> Program.watchFile(file: File, transducer: (File) -> T): () -> T = watchFile(this, file, transducer)
private val watching = mutableMapOf<Path, MutableList<FileWatcher>>()
private val pathKeys = mutableMapOf<Path, WatchKey>()
private val keyPaths = mutableMapOf<WatchKey, Path>()
private val waiting = mutableMapOf<Path, Job>()
private val watchService by lazy {
FileSystems.getDefault().newWatchService()
}
private val watchThread by lazy {
thread(isDaemon = true) {
while (true) {
val key = watchService.take()
val path = keyPaths[key]
key.pollEvents().forEach {
val contextPath = it.context() as Path
val fullPath = path?.resolve(contextPath)
fullPath?.let {
waiting[fullPath]?.cancel()
waiting[fullPath] = GlobalScope.launch {
delay(100)
watching[fullPath]?.forEach { w ->
w.triggerChange()
}
}
}
}
key.reset()
}
}
}
fun main() {
val a = watchFile(Program(), File("README.md")) {
it.readText()
}
a.stop()
a.triggerChange()
while (true) {
println(a())
Thread.sleep(2000)
}
}