coroutine based file watcher (supposed to be used for etag stuff, but it's a bit janky)
This commit is contained in:
parent
23d77ae26c
commit
008c25233d
1 changed files with 195 additions and 0 deletions
|
|
@ -0,0 +1,195 @@
|
||||||
|
package dev.adamko.kafkatorio.processor.misc
|
||||||
|
|
||||||
|
import dev.adamko.kafkatorio.processor.KafkatorioTopology
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
||||||
|
import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE
|
||||||
|
import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY
|
||||||
|
import java.nio.file.WatchEvent
|
||||||
|
import java.nio.file.WatchKey
|
||||||
|
import java.nio.file.WatchService
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.io.path.isReadable
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineName
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.cancellable
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
|
||||||
|
|
||||||
|
fun File.fileEventsFlow() = FileEventsFlow(
|
||||||
|
root = this.toPath(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Path.fileEventsFlow() = FileEventsFlow(
|
||||||
|
root = this,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/** [SharedFlow] wrapper for Java's [WatchService] */
|
||||||
|
class FileEventsFlow private constructor(
|
||||||
|
private val root: Path,
|
||||||
|
private val fileEvents: MutableSharedFlow<KWatchEvent>,
|
||||||
|
) : SharedFlow<KWatchEvent> by fileEvents, CoroutineScope {
|
||||||
|
|
||||||
|
constructor(root: Path) : this(root, MutableSharedFlow(0, Int.MAX_VALUE, BufferOverflow.SUSPEND))
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext =
|
||||||
|
CoroutineName("FileEventsFlow") +
|
||||||
|
KafkatorioTopology.rootJob +
|
||||||
|
Dispatchers.Unconfined +
|
||||||
|
CoroutineExceptionHandler { _, e ->
|
||||||
|
println("handling exception in FileEventsFlow")
|
||||||
|
e.printStackTrace()
|
||||||
|
watchService.close()
|
||||||
|
watchKeys.update {
|
||||||
|
it.cancelAll()
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
|
||||||
|
|
||||||
|
private val watchKeys: MutableStateFlow<List<WatchKey>> = MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
println("launching FileEventsFlow... $root")
|
||||||
|
registerPaths()
|
||||||
|
pollFileEvents()
|
||||||
|
|
||||||
|
launch {
|
||||||
|
fileEvents.emit(KWatchEvent.Initialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
println("launched FileEventsFlow $root")
|
||||||
|
|
||||||
|
root.toFile().walk()
|
||||||
|
// .filter { it.isFile }
|
||||||
|
.onEach {
|
||||||
|
launch {
|
||||||
|
fileEvents.emit(KWatchEvent.Created(it.toPath()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.count()
|
||||||
|
.let {
|
||||||
|
println("initializing KWatchEvent.Created $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Watch for any file tree changes and re-register the [watchKeys] */
|
||||||
|
private fun registerPaths() = launch {
|
||||||
|
println("launching registerPaths")
|
||||||
|
fileEvents
|
||||||
|
// .onEach { println("file event! $it") }
|
||||||
|
.filter { event ->
|
||||||
|
when (event) {
|
||||||
|
is KWatchEvent.Initialized -> true
|
||||||
|
|
||||||
|
is KWatchEvent.Created -> event.path.isDirectory
|
||||||
|
is KWatchEvent.Deleted -> event.path.isDirectory
|
||||||
|
|
||||||
|
is KWatchEvent.Modified -> false
|
||||||
|
}
|
||||||
|
}.debounce(1.seconds)
|
||||||
|
.runningFold(emptyList<WatchKey>()) { previousKeys, directoryTreeAlteredEvent ->
|
||||||
|
println("re-registering paths $directoryTreeAlteredEvent")
|
||||||
|
|
||||||
|
previousKeys.cancelAll()
|
||||||
|
|
||||||
|
root.toFile().walk()
|
||||||
|
.filter { it.isDirectory }
|
||||||
|
.map {
|
||||||
|
// println("re-registering watch on $it")
|
||||||
|
it.toPath().register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
.also {
|
||||||
|
println("${it.count()} registered watches")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cancellable()
|
||||||
|
.collect(watchKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Poll [watchService] for updates while [coroutineContext] is active. */
|
||||||
|
private fun pollFileEvents() = launch {
|
||||||
|
flow<WatchKey> {
|
||||||
|
while (coroutineContext.isActive) {
|
||||||
|
emit(
|
||||||
|
withContext(Dispatchers.IO) { watchService.take() }
|
||||||
|
)
|
||||||
|
println("emitted watchService.take()")
|
||||||
|
yield()
|
||||||
|
}
|
||||||
|
}.mapNotNull { monitorKey ->
|
||||||
|
when (val keyPath = monitorKey.watchable()) {
|
||||||
|
is Path -> monitorKey to keyPath
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.filter { it.second.isReadable() }
|
||||||
|
.flatMapConcat { (monitorKey: WatchKey, keyPath: Path) ->
|
||||||
|
val events = monitorKey.pollEvents()
|
||||||
|
monitorKey.reset()
|
||||||
|
events
|
||||||
|
.onEach { println("monitorKey event $it") }
|
||||||
|
.filter { it.context() as? Path != null }
|
||||||
|
.mapNotNull { event ->
|
||||||
|
val eventPath = keyPath.resolve(event.context() as Path)
|
||||||
|
|
||||||
|
when (event.kind()) {
|
||||||
|
ENTRY_CREATE -> KWatchEvent.Created(eventPath)
|
||||||
|
ENTRY_DELETE -> KWatchEvent.Deleted(eventPath)
|
||||||
|
ENTRY_MODIFY -> KWatchEvent.Modified(eventPath)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.asFlow()
|
||||||
|
}.cancellable()
|
||||||
|
.collect(fileEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val Path.isDirectory: Boolean get() = Files.isDirectory(this)
|
||||||
|
|
||||||
|
private fun List<WatchKey>.cancelAll() = forEach { it.cancel(); it.pollEvents() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wrapper around [WatchEvent] that comes with properly resolved absolute path */
|
||||||
|
sealed interface KWatchEvent {
|
||||||
|
|
||||||
|
object Initialized : KWatchEvent
|
||||||
|
|
||||||
|
sealed interface PathEvent : KWatchEvent {
|
||||||
|
/** path of modified file or directory */
|
||||||
|
val path: Path
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Created(override val path: Path) : PathEvent
|
||||||
|
data class Modified(override val path: Path) : PathEvent
|
||||||
|
data class Deleted(override val path: Path) : PathEvent
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue