diff --git a/common/src/main/java/taboolib/common/FileWatcher.kt b/common/src/main/java/taboolib/common/FileWatcher.kt new file mode 100644 index 000000000..63600c448 --- /dev/null +++ b/common/src/main/java/taboolib/common/FileWatcher.kt @@ -0,0 +1,206 @@ +package taboolib.common + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.* +import java.nio.file.WatchService +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +/** + * 文件事件密封类,表示文件系统的变化事件 + * + * @property file 发生变化的文件 + */ +sealed class FileEvent(val file: File) { + /** + * 文件创建事件 + */ + class Create(file: File) : FileEvent(file) + + /** + * 文件修改事件 + */ + class Modify(file: File) : FileEvent(file) + + /** + * 文件删除事件 + */ + class Delete(file: File) : FileEvent(file) +} + +/** + * 监视指定文件夹的变化,返回文件事件流 + * + * 该函数会创建一个独立的监视线程,使用 Java NIO WatchService 监听文件夹的创建、修改和删除事件。 + * 返回的 Flow 会持续发出文件事件,直到 Flow 被取消。 + * + * @param path 要监视的文件夹路径 + * @return 文件事件流,发出 [FileEvent.Create]、[FileEvent.Modify] 或 [FileEvent.Delete] 事件 + */ +fun watchFolder(path: Path): Flow { + return callbackFlow { + val watchThread = object : Thread("FileWatcher-${path.fileName}") { + + private val running = AtomicBoolean(true) + private lateinit var watchService: WatchService + + /** + * 注销文件监视器,停止监视线程 + */ + fun unregisterWatcher() { + running.set(false) + + runCatching { + watchService.close() + } + } + + override fun run() { + runCatching { + val fileSystem = FileSystems.getDefault() + watchService = fileSystem.newWatchService() + + // 注册文件夹的创建、修改、删除事件 + path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) + + while (running.get()) { + // 每5秒轮询一次事件 + val key = watchService.poll(5, TimeUnit.SECONDS) ?: continue + val watchedPath = key.watchable() as Path + + // 处理所有待处理的事件 + for (event in key.pollEvents()) { + val file = watchedPath.resolve(event.context() as Path).toFile() + when (event.kind()) { + ENTRY_CREATE -> channel.trySendBlocking(FileEvent.Create(file)) + ENTRY_MODIFY -> channel.trySendBlocking(FileEvent.Modify(file)) + ENTRY_DELETE -> channel.trySendBlocking(FileEvent.Delete(file)) + } + } + + // 重置 key,以便继续接收事件 + if (!key.reset()) { + // key 无效,可能是目录被删除了 + break + } + } + }.onFailure { throwable -> + // 发生异常时关闭 channel + channel.close(throwable) + } + } + } + + watchThread.start() + + // 当 Flow 被取消时,清理资源 + awaitClose { + watchThread.unregisterWatcher() + if (watchThread.isAlive) watchThread.interrupt() + } + } +} + +/** + * 监视指定文件的变化 + * + * 该函数会监视单个文件的变化事件。如果该文件所在的父文件夹已经在被监视中, + * 则直接添加回调函数到现有的监视流中;否则创建一个新的 [WatchFlow]。 + * + * @param filePath 要监视的文件路径 + * @param func 文件事件回调函数,当文件发生变化时调用 + * @return 如果创建了新的监视流,则返回 [WatchFlow];如果复用了现有监视流,则返回 null + */ +fun watchFile(filePath: Path, func: (event: FileEvent) -> Unit): WatchFlow? { + val parent = filePath.parent + // 注册文件的监视回调 + FileWatcher.watchingFiles.getOrPut(parent) { ConcurrentHashMap() }[filePath] = func + + // 如果父文件夹已在监视中,直接复用 + return if (FileWatcher.watchingFolderJob.containsKey(parent)) { + null + } else { + // 创建新的文件夹监视流 + WatchFlow(parent, watchFolder(parent).onEach { event -> + FileWatcher.watchingFiles[parent]?.get(event.file.toPath())?.invoke(event) + }) + } +} + +/** + * 停止监视指定文件 + * + * 移除文件的监视回调,如果该文件所在的父文件夹没有其他文件在被监视, + * 则同时取消父文件夹的监视任务。 + * + * @param filePath 要停止监视的文件路径 + */ +fun stopWatchingFile(filePath: Path) { + FileWatcher.watchingFiles[filePath.parent]?.remove(filePath) + // 如果父文件夹下没有其他文件在被监视,则取消文件夹的监视任务 + if (FileWatcher.watchingFiles[filePath.parent]?.isEmpty() == true) { + FileWatcher.watchingFolderJob.remove(filePath.parent)?.cancel() + } +} + +/** + * 文件监视流包装类 + * + * 包装了文件夹的监视流,提供便捷的启动方法,并管理监视任务的生命周期。 + * + * @property path 被监视的文件夹路径 + * @property flow 文件事件流 + */ +class WatchFlow(val path: Path, val flow: Flow): Flow by flow { + + /** + * 在指定的协程作用域中启动监视流 + * + * 如果该文件夹已经有正在运行的监视任务,会先取消旧任务,然后启动新任务。 + * + * @param scope 协程作用域 + * @return 启动的协程 Job + */ + fun start(scope: CoroutineScope): Job { + FileWatcher.watchingFolderJob[path]?.cancel() + val job = flow.launchIn(scope) + FileWatcher.watchingFolderJob[path] = job + return job + } +} + +/** + * 文件监视器管理对象 + * + * 用于管理所有正在运行的文件监视任务和回调函数。 + * 采用文件夹级别的监视策略,多个文件可以共享同一个文件夹监视流。 + */ +object FileWatcher { + + /** + * 文件夹监视任务映射表 + * + * Key: 文件夹路径 + * Value: 监视任务的协程 Job + */ + val watchingFolderJob = ConcurrentHashMap() + + /** + * 文件监视回调映射表 + * + * Key: 父文件夹路径 + * Value: 该文件夹下被监视的文件及其对应的回调函数 + */ + val watchingFiles = ConcurrentHashMap Unit>>() +} \ No newline at end of file diff --git a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigLoader.kt b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigLoader.kt index 402060167..d4e87abfa 100644 --- a/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigLoader.kt +++ b/module/basic/basic-configuration/src/main/kotlin/taboolib/module/configuration/ConfigLoader.kt @@ -1,17 +1,18 @@ package taboolib.module.configuration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import org.tabooproject.reflex.ClassField import org.tabooproject.reflex.ReflexClass -import taboolib.common.Inject -import taboolib.common.LifeCycle -import taboolib.common.PrimitiveIO +import taboolib.common.* import taboolib.common.env.RuntimeDependencies import taboolib.common.env.RuntimeDependency import taboolib.common.inject.ClassVisitor import taboolib.common.platform.Awake import taboolib.common.platform.PlatformFactory import taboolib.common.platform.function.releaseResourceFile -import taboolib.common5.FileWatcher @RuntimeDependencies( RuntimeDependency( @@ -71,12 +72,17 @@ class ConfigLoader : ClassVisitor(1) { // 自动重载 if (configAnno.property("autoReload", false)) { PrimitiveIO.debug("正在监听文件变更: ${file.absolutePath}") - FileWatcher.INSTANCE.addSimpleListener(file) { - PrimitiveIO.debug("文件变更: ${file.absolutePath}") + + watchFile(file.toPath()) { event -> + when (event) { + is FileEvent.Create -> PrimitiveIO.debug("文件创建: ${event.file.absolutePath}") + is FileEvent.Modify -> PrimitiveIO.debug("文件修改: ${event.file.absolutePath}") + is FileEvent.Delete -> PrimitiveIO.debug("文件删除: ${event.file.absolutePath}") + } if (file.exists()) { conf.loadFromFile(file) } - } + }?.start(scope) } val configFile = ConfigNodeFile(conf, file) conf.onReload { @@ -97,5 +103,12 @@ class ConfigLoader : ClassVisitor(1) { companion object { val files = HashMap() + + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + @Awake(LifeCycle.DISABLE) + private fun disable() { + scope.cancel("server disable") + } } } \ No newline at end of file diff --git a/module/minecraft/minecraft-i18n/src/main/kotlin/taboolib/module/lang/ResourceReader.kt b/module/minecraft/minecraft-i18n/src/main/kotlin/taboolib/module/lang/ResourceReader.kt index 82aa8f06d..2650821bd 100644 --- a/module/minecraft/minecraft-i18n/src/main/kotlin/taboolib/module/lang/ResourceReader.kt +++ b/module/minecraft/minecraft-i18n/src/main/kotlin/taboolib/module/lang/ResourceReader.kt @@ -2,15 +2,19 @@ package taboolib.module.lang +import taboolib.common.FileEvent +import taboolib.common.PrimitiveIO import taboolib.common.io.newFile import taboolib.common.io.runningResourcesInJar import taboolib.common.platform.function.pluginId import taboolib.common.platform.function.submitAsync import taboolib.common.platform.function.warning +import taboolib.common.stopWatchingFile import taboolib.common.util.replaceWithOrder import taboolib.common.util.t -import taboolib.common5.FileWatcher +import taboolib.common.watchFile import taboolib.library.configuration.ConfigurationSection +import taboolib.module.configuration.ConfigLoader.Companion.scope import taboolib.module.configuration.Configuration import taboolib.module.configuration.SecuredFile import java.io.File @@ -47,7 +51,7 @@ class ResourceReader(val clazz: Class<*>, val migrate: Boolean = true) { } // 移除文件监听 if (Language.enableFileWatcher) { - FileWatcher.INSTANCE.removeListener(file) + stopWatchingFile(file.toPath()) } val exists = HashMap() // 加载文件 @@ -63,11 +67,17 @@ class ResourceReader(val clazz: Class<*>, val migrate: Boolean = true) { files[code] = it // 文件变动监听 if (Language.enableFileWatcher) { - FileWatcher.INSTANCE.addSimpleListener(file) { _ -> + + watchFile(file.toPath()) { event -> + when (event) { + is FileEvent.Create -> PrimitiveIO.debug("文件创建: ${event.file.absolutePath}") + is FileEvent.Modify -> PrimitiveIO.debug("文件修改: ${event.file.absolutePath}") + is FileEvent.Delete -> PrimitiveIO.debug("文件删除: ${event.file.absolutePath}") + } it.nodes.clear() loadNodes(sourceFile, it.nodes, code) loadNodes(Configuration.loadFromFile(file), it.nodes, code) - } + }?.start(scope) } } } else {