From 139c07de18bb23a21dd54719c732835745351c81 Mon Sep 17 00:00:00 2001 From: zhibei <785740487@qq.com> Date: Thu, 20 Nov 2025 12:01:08 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(common):=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E5=8D=8F=E7=A8=8B=E7=9A=84=E6=96=87=E4=BB=B6=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E5=99=A8=20`FileWatcher`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(configuration): 使用新的文件监听器替换 `FileWatcher.INSTANCE` refactor(i18n): 使用新的文件监听器替换 `FileWatcher.INSTANCE` chore(configuration): 添加协程作用域 `scope` 并在服务禁用时取消 fix(configuration): 优化导入语句结构 fix(i18n): 添加必要的导入语句 --- .../main/java/taboolib/common/FileWatcher.kt | 106 ++++++++++++++++++ .../module/configuration/ConfigLoader.kt | 27 +++-- .../taboolib/module/lang/ResourceReader.kt | 18 ++- 3 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 common/src/main/java/taboolib/common/FileWatcher.kt 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..766648728 --- /dev/null +++ b/common/src/main/java/taboolib/common/FileWatcher.kt @@ -0,0 +1,106 @@ +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.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +sealed class FileEvent(val file: File) { + class Create(file: File) : FileEvent(file) + class Modify(file: File) : FileEvent(file) + class Delete(file: File) : FileEvent(file) +} + +fun watchFolder(path: Path): Flow { + return callbackFlow { + val watchThread = object : Thread() { + + private val running = AtomicBoolean(true) + private lateinit var watchService: WatchService + + fun unregisterWatcher() { + running.set(false) + + runCatching { + watchService.close() + } + } + + override fun run() { + val fileSystem = FileSystems.getDefault() + watchService = fileSystem.newWatchService() + + path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) + + while (running.get()) { + val key = watchService.poll(5, TimeUnit.SECONDS) ?: continue + val path = key.watchable() as Path + + for (event in key.pollEvents()) { + val file = path.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.reset() + } + } + } + + watchThread.start() + + awaitClose { + watchThread.unregisterWatcher() + if (watchThread.isAlive) watchThread.interrupt() + } + } +} + +fun watchFile(filePath: Path, func: (event: FileEvent) -> Unit): WatchFlow? { + val parent = filePath.parent + return if (FileWatcher.watchingFolderJob.containsKey(parent)) { + FileWatcher.watchingFiles.getOrPut(parent) { hashMapOf() }[filePath] = func + null + } else { + FileWatcher.watchingFiles.getOrPut(parent) { hashMapOf() }[filePath] = func + WatchFlow(parent, watchFolder(parent).onEach { event -> + FileWatcher.watchingFiles[parent]?.get(event.file.toPath())?.invoke(event) + }) + } +} + +fun stopWatching(filePath: Path) { + FileWatcher.watchingFiles[filePath.parent]?.remove(filePath) + if (FileWatcher.watchingFiles[filePath.parent]?.isEmpty() == true) { + FileWatcher.watchingFolderJob.remove(filePath.parent)?.cancel() + } +} + +class WatchFlow(val path: Path, val flow: Flow): Flow by flow { + + fun launchIn(scope: CoroutineScope) { + FileWatcher.watchingFolderJob[path]?.cancel() + FileWatcher.watchingFolderJob[path] = flow.launchIn(scope) + } +} + +object FileWatcher { + + val watchingFolderJob = mutableMapOf() + + val watchingFiles = mutableMapOf 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..6d729d30e 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) } - } + }?.launchIn(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..809c26f0e 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.stopWatching 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) + stopWatching(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) - } + }?.launchIn(scope) } } } else { From 7ccba8f0dcf238b23751836d47e93294a959996e Mon Sep 17 00:00:00 2001 From: zhibei <785740487@qq.com> Date: Thu, 20 Nov 2025 13:11:24 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(common):=20=E5=B0=86=20`stopWatching`?= =?UTF-8?q?=20=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA=20`stopWatchingFile`=20?= =?UTF-8?q?=E4=BB=A5=E5=8C=B9=E9=85=8D=E6=96=B0=E5=87=BD=E6=95=B0=E5=90=8D?= =?UTF-8?q?=20refactor(common):=20=E9=87=8D=E6=9E=84=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9B=91=E8=A7=86=E5=99=A8=E4=BB=A5=E5=A2=9E=E5=BC=BA=E5=8F=AF?= =?UTF-8?q?=E9=9D=A0=E6=80=A7=E5=92=8C=E5=B9=B6=E5=8F=91=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7=20docs(common):=20=E4=B8=BA=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9B=91=E8=A7=86=E5=99=A8=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E7=9A=84Kotlin=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/taboolib/common/FileWatcher.kt | 150 +++++++++++++++--- .../taboolib/module/lang/ResourceReader.kt | 4 +- 2 files changed, 127 insertions(+), 27 deletions(-) diff --git a/common/src/main/java/taboolib/common/FileWatcher.kt b/common/src/main/java/taboolib/common/FileWatcher.kt index 766648728..63600c448 100644 --- a/common/src/main/java/taboolib/common/FileWatcher.kt +++ b/common/src/main/java/taboolib/common/FileWatcher.kt @@ -13,22 +13,51 @@ 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() { + val watchThread = object : Thread("FileWatcher-${path.fileName}") { private val running = AtomicBoolean(true) private lateinit var watchService: WatchService + /** + * 注销文件监视器,停止监视线程 + */ fun unregisterWatcher() { running.set(false) @@ -38,31 +67,44 @@ fun watchFolder(path: Path): Flow { } override fun run() { - val fileSystem = FileSystems.getDefault() - watchService = fileSystem.newWatchService() - - path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) - - while (running.get()) { - val key = watchService.poll(5, TimeUnit.SECONDS) ?: continue - val path = key.watchable() as Path + 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)) + } + } - for (event in key.pollEvents()) { - val file = path.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 } } - - key.reset() + }.onFailure { throwable -> + // 发生异常时关闭 channel + channel.close(throwable) } } } watchThread.start() + // 当 Flow 被取消时,清理资源 awaitClose { watchThread.unregisterWatcher() if (watchThread.isAlive) watchThread.interrupt() @@ -70,37 +112,95 @@ fun watchFolder(path: Path): Flow { } } +/** + * 监视指定文件的变化 + * + * 该函数会监视单个文件的变化事件。如果该文件所在的父文件夹已经在被监视中, + * 则直接添加回调函数到现有的监视流中;否则创建一个新的 [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)) { - FileWatcher.watchingFiles.getOrPut(parent) { hashMapOf() }[filePath] = func null } else { - FileWatcher.watchingFiles.getOrPut(parent) { hashMapOf() }[filePath] = func + // 创建新的文件夹监视流 WatchFlow(parent, watchFolder(parent).onEach { event -> FileWatcher.watchingFiles[parent]?.get(event.file.toPath())?.invoke(event) }) } } -fun stopWatching(filePath: Path) { +/** + * 停止监视指定文件 + * + * 移除文件的监视回调,如果该文件所在的父文件夹没有其他文件在被监视, + * 则同时取消父文件夹的监视任务。 + * + * @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 { - fun launchIn(scope: CoroutineScope) { + /** + * 在指定的协程作用域中启动监视流 + * + * 如果该文件夹已经有正在运行的监视任务,会先取消旧任务,然后启动新任务。 + * + * @param scope 协程作用域 + * @return 启动的协程 Job + */ + fun start(scope: CoroutineScope): Job { FileWatcher.watchingFolderJob[path]?.cancel() - FileWatcher.watchingFolderJob[path] = flow.launchIn(scope) + val job = flow.launchIn(scope) + FileWatcher.watchingFolderJob[path] = job + return job } } +/** + * 文件监视器管理对象 + * + * 用于管理所有正在运行的文件监视任务和回调函数。 + * 采用文件夹级别的监视策略,多个文件可以共享同一个文件夹监视流。 + */ object FileWatcher { - val watchingFolderJob = mutableMapOf() - - val watchingFiles = mutableMapOf Unit>>() + /** + * 文件夹监视任务映射表 + * + * Key: 文件夹路径 + * Value: 监视任务的协程 Job + */ + val watchingFolderJob = ConcurrentHashMap() + + /** + * 文件监视回调映射表 + * + * Key: 父文件夹路径 + * Value: 该文件夹下被监视的文件及其对应的回调函数 + */ + val watchingFiles = ConcurrentHashMap Unit>>() } \ 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 809c26f0e..df7478490 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 @@ -9,7 +9,7 @@ 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.stopWatching +import taboolib.common.stopWatchingFile import taboolib.common.util.replaceWithOrder import taboolib.common.util.t import taboolib.common.watchFile @@ -51,7 +51,7 @@ class ResourceReader(val clazz: Class<*>, val migrate: Boolean = true) { } // 移除文件监听 if (Language.enableFileWatcher) { - stopWatching(file.toPath()) + stopWatchingFile(file.toPath()) } val exists = HashMap() // 加载文件 From 8bbaa484be461dc323572ba06f22033d901baba6 Mon Sep 17 00:00:00 2001 From: zhibei <785740487@qq.com> Date: Thu, 20 Nov 2025 13:12:27 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(configuration):=20=E5=B0=86=E5=8D=8F?= =?UTF-8?q?=E7=A8=8B=E5=90=AF=E5=8A=A8=E6=96=B9=E6=B3=95=E4=BB=8E=20`launc?= =?UTF-8?q?hIn`=20=E6=9B=B4=E6=94=B9=E4=B8=BA=20`start`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(lang): 将协程启动方法从 `launchIn` 更改为 `start` --- .../main/kotlin/taboolib/module/configuration/ConfigLoader.kt | 2 +- .../src/main/kotlin/taboolib/module/lang/ResourceReader.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 6d729d30e..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 @@ -82,7 +82,7 @@ class ConfigLoader : ClassVisitor(1) { if (file.exists()) { conf.loadFromFile(file) } - }?.launchIn(scope) + }?.start(scope) } val configFile = ConfigNodeFile(conf, file) conf.onReload { 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 df7478490..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 @@ -77,7 +77,7 @@ class ResourceReader(val clazz: Class<*>, val migrate: Boolean = true) { it.nodes.clear() loadNodes(sourceFile, it.nodes, code) loadNodes(Configuration.loadFromFile(file), it.nodes, code) - }?.launchIn(scope) + }?.start(scope) } } } else {