package org.openrndr.extra.filewatcher import com.sun.nio.file.SensitivityWatchEventModifier import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.* import org.openrndr.events.Event 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 java.util.WeakHashMap import kotlin.concurrent.thread private val logger = KotlinLogging.logger {} private val watching = mutableMapOf>() private val pathKeys = mutableMapOf() private val keyPaths = WeakHashMap() private val waiting = mutableMapOf() private val watchService by lazy { FileSystems.getDefault().newWatchService() } @OptIn(DelicateCoroutinesApi::class) 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() } } } /** * @property file * @property fileChangedEvent * @param requestStopEvent */ class FileWatcher( private val file: File, private val fileChangedEvent: Event, requestStopEvent: Event? = null ) { private val path = file.absoluteFile.toPath() private val parent = path.parent private val key = pathKeys.getOrPut(parent) { parent.register( watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY), SensitivityWatchEventModifier.HIGH ) } init { watchThread watching.getOrPut(path) { mutableListOf() }.add(this) keyPaths.getOrPut(key) { parent } requestStopEvent?.listenOnce { stop() } } @Suppress("MemberVisibilityCanBePrivate") fun stop() { synchronized(watching) { logger.info { "stopping, watcher stop requested" } watching[path]?.remove(this) } } internal fun triggerChange() { fileChangedEvent.trigger(file) } } /** * Watch a file for changes * @param file the file to watch * @param valueChangedEvent the event that is triggered when the value (after transforming) has changed * @param requestStopEvent an event that can be triggered to request the watcher to stop * @param transducer a function that transforms a [File] into a value of type [R] */ fun watchFile( file: File, valueChangedEvent: Event? = null, requestStopEvent: Event? = null, transducer: (File) -> R ): () -> R { var result = transducer(file) val fileChangedEvent = Event() @Suppress("UNUSED_VARIABLE") val watcher = FileWatcher(file, fileChangedEvent, requestStopEvent) fileChangedEvent.listen { @Suppress("MemberVisibilityCanBePrivate") try { result = transducer(file) valueChangedEvent?.trigger(result) } catch (e: Throwable) { logger.error(e) { """exception while transforming file ${file.absolutePath}""" } } } return { result } }