Skip to content

Commit 851ffd7

Browse files
committed
feat: automatic reloadable configs
1 parent 6abdae6 commit 851ffd7

File tree

15 files changed

+234
-93
lines changed

15 files changed

+234
-93
lines changed

proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/ProxyBungeeCordPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class ProxyBungeeCordPlugin: Plugin() {
4141
this.proxy.pluginManager.registerListener(this, ConfigureTagResolversListener(this))
4242
this.proxy.pluginManager.registerListener(this, ServerPreConnectListener(this))
4343

44-
if (this.proxyPlugin.tabListConfiguration.tabListUpdateTime > 0)
44+
if (this.proxyPlugin.tabListConfiguration.get().tabListUpdateTime > 0)
4545
this.tabListHandler.startTabListTask()
4646
else
4747
this.logger.info("Tablist update time is set to 0, tablist will not be updated automatically")

proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/handler/TabListHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class TabListHandler(
2121
this.tabListIndex.forEach { (key, value) ->
2222
this.tabListIndex[key] = value + 1
2323
}
24-
}, 1, this.plugin.proxyPlugin.tabListConfiguration.tabListUpdateTime, TimeUnit.MILLISECONDS)
24+
}, 1, this.plugin.proxyPlugin.tabListConfiguration.get().tabListUpdateTime, TimeUnit.MILLISECONDS)
2525
}
2626

2727
fun stopTabListTask() {
@@ -37,7 +37,7 @@ class TabListHandler(
3737
if (player.server == null) return
3838
if (player.server.info == null) return
3939

40-
val configuration = plugin.proxyPlugin.tabListConfiguration
40+
val configuration = plugin.proxyPlugin.tabListConfiguration.get()
4141
val serviceName = player.server.info.name
4242

4343
var tabListGroup = configuration.groups.find { group ->

proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ConfigureTagResolversListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ConfigureTagResolversListener(
2020
val serverName = player?.server?.info?.name ?: "unknown"
2121

2222
val ping = player?.ping ?: -1
23-
val pingColors = plugin.proxyPlugin.placeHolderConfiguration.pingColors
23+
val pingColors = plugin.proxyPlugin.placeHolderConfiguration.get().pingColors
2424

2525
val onlinePlayers = plugin.proxy.players.size
2626
val realMaxPlayers = plugin.proxy.config.playerLimit

proxy-bungeecord/src/main/kotlin/app/simplecloud/plugin/proxy/bungeecord/listener/ServerPreConnectListener.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ServerPreConnectListener(
1919
private val logger = Logger.getLogger(ServerPreConnectListener::class.java.name)
2020

2121
private val identifier = ServerPatternIdentifier(
22-
this.proxyPlugin.proxyPlugin.joinStateConfiguration.serverNamePattern
22+
this.proxyPlugin.proxyPlugin.joinStateConfiguration.get().serverNamePattern
2323
)
2424

2525
@EventHandler(priority = EventPriority.HIGH)
@@ -33,19 +33,19 @@ class ServerPreConnectListener(
3333

3434
private fun checkAllowProxyJoin(player: ProxiedPlayer, event: ServerConnectEvent) {
3535
val localState = this.proxyPlugin.proxyPlugin.joinStateHandler.localState
36-
val joinState = this.proxyPlugin.proxyPlugin.joinStateConfiguration.joinStates.find { it.name == localState }
36+
val joinState = this.proxyPlugin.proxyPlugin.joinStateConfiguration.get().joinStates.find { it.name == localState }
3737

3838
if (joinState == null) {
3939
logger.info("The join state for the proxy could not be found.")
40-
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.kickMessage.noJoinState, false, event)
40+
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.get().kickMessage.noJoinState, false, event)
4141
return
4242
}
4343

4444
if (!player.hasPermission(joinState.joinPermission) && joinState.joinPermission.trim().isNotEmpty()) {
4545
logger.info("The player ${player.name} does not have the permission to join the proxy and will be kicked.")
4646
denyAccess(
4747
player,
48-
this.proxyPlugin.proxyPlugin.messagesConfiguration.kickMessage.noPermission,
48+
this.proxyPlugin.proxyPlugin.messagesConfiguration.get().kickMessage.noPermission,
4949
false,
5050
event
5151
)
@@ -61,7 +61,7 @@ class ServerPreConnectListener(
6161
if (player.hasPermission(joinState.fullJoinPermission)) {
6262
return@runBlocking
6363
}
64-
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.kickMessage.networkFull, false, event)
64+
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.get().kickMessage.networkFull, false, event)
6565
} catch (e: Exception) {
6666
logger.severe("Error checking player limits: ${e.message}")
6767
}
@@ -76,19 +76,19 @@ class ServerPreConnectListener(
7676
runBlocking {
7777
val joinStateName =
7878
proxyPlugin.proxyPlugin.joinStateHandler.getJoinStateAtService(groupName, numericalId.toLong())
79-
val joinState = proxyPlugin.proxyPlugin.joinStateConfiguration.joinStates.find { it.name == joinStateName }
79+
val joinState = proxyPlugin.proxyPlugin.joinStateConfiguration.get().joinStates.find { it.name == joinStateName }
8080

8181
if (joinState == null) {
8282
logger.warning("The join state for the server $serverName could not be found.")
83-
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.kickMessage.noJoinState, true, event)
83+
denyAccess(player, proxyPlugin.proxyPlugin.messagesConfiguration.get().kickMessage.noJoinState, true, event)
8484
return@runBlocking
8585
}
8686

8787
if (joinState.joinPermission.trim().isNotEmpty() && !player.hasPermission(joinState.joinPermission)) {
8888
logger.info("The player ${player.name} does not have the permission to join $serverName and will be kicked.")
8989
denyAccess(
9090
player,
91-
proxyPlugin.proxyPlugin.messagesConfiguration.kickMessage.noPermission,
91+
proxyPlugin.proxyPlugin.messagesConfiguration.get().kickMessage.noPermission,
9292
true,
9393
event
9494
)

proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/ProxyPlugin.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@ import app.simplecloud.plugin.proxy.shared.config.tablis.TabListConfiguration
88
import app.simplecloud.plugin.proxy.shared.handler.CloudControllerHandler
99
import app.simplecloud.plugin.proxy.shared.handler.JoinStateHandler
1010
import app.simplecloud.plugin.proxy.shared.handler.MotdLayoutHandler
11+
import java.io.File
1112

1213
open class ProxyPlugin(
1314
dirPath: String
1415
) {
1516

1617
val config = YamlConfig(dirPath)
17-
val tabListConfiguration = config.load<TabListConfiguration>("tablist")!!
18-
val placeHolderConfiguration = config.load<PlaceHolderConfiguration>("placeholder")!!
19-
val messagesConfiguration = config.load<MessageConfig>("messages")!!
20-
val joinStateConfiguration = config.load<JoinStateConfiguration>("joinstate")!!
18+
val tabListConfiguration = config.load<TabListConfiguration>("tablist")
19+
val placeHolderConfiguration = config.load<PlaceHolderConfiguration>("placeholder")
20+
val messagesConfiguration = config.load<MessageConfig>("messages")
21+
val joinStateConfiguration = config.load<JoinStateConfiguration>("joinstate")
2122

22-
val motdLayoutHandler = MotdLayoutHandler(config, this)
23+
val motdLayoutHandler = MotdLayoutHandler(File(dirPath + "/layout").toPath(), this)
2324
val joinStateHandler = JoinStateHandler(this)
2425
val cloudControllerHandler = CloudControllerHandler(joinStateHandler)
2526

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,79 @@
11
package app.simplecloud.plugin.proxy.shared.config
22

3+
import app.simplecloud.plugin.proxy.shared.config.reactive.ReactiveConfig
4+
import app.simplecloud.plugin.proxy.shared.config.reactive.ReactiveConfigInfo
5+
import kotlinx.coroutines.*
36
import org.spongepowered.configurate.CommentedConfigurationNode
4-
import org.spongepowered.configurate.kotlin.extensions.get
57
import org.spongepowered.configurate.kotlin.objectMapperFactory
8+
import org.spongepowered.configurate.loader.ParsingException
69
import org.spongepowered.configurate.yaml.NodeStyle
710
import org.spongepowered.configurate.yaml.YamlConfigurationLoader
811
import java.io.File
12+
import java.nio.file.*
13+
import java.util.concurrent.ConcurrentHashMap
914

1015
open class YamlConfig(val dirPath: String) {
1116

12-
inline fun <reified T> load(): T? {
17+
private val watchService = FileSystems.getDefault().newWatchService()
18+
private val configCache = ConcurrentHashMap<String, Any>()
19+
private val reactiveConfigs = ConcurrentHashMap<String, MutableList<ReactiveConfigInfo<*>>>()
20+
private var watcherJob: Job? = null
21+
22+
init {
23+
startWatcher()
24+
}
25+
26+
inline fun <reified T> load(): ReactiveConfig<T> {
1327
return load(null)
1428
}
1529

30+
inline fun <reified T> load(path: String?): ReactiveConfig<T> {
31+
return ReactiveConfig(this, path, T::class.java)
32+
}
33+
34+
internal fun <T> loadDirect(path: String?, clazz: Class<T>): T? {
35+
val cacheKey = path ?: "default"
36+
37+
try {
38+
val node = buildNode(path).first
39+
val config = node.get(clazz)
40+
41+
if (config != null) {
42+
configCache[cacheKey] = config
43+
}
44+
45+
return config
46+
} catch (ex: ParsingException) {
47+
val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath)
48+
println("Could not load config file ${file.name}. Using cached version if available.")
49+
@Suppress("UNCHECKED_CAST")
50+
return configCache[cacheKey] as? T
51+
}
52+
}
53+
54+
internal fun <T> registerReactiveConfig(path: String?, clazz: Class<T>, reactiveConfig: ReactiveConfig<T>) {
55+
val cacheKey = path ?: "default"
56+
val configs = reactiveConfigs.getOrPut(cacheKey) { mutableListOf() }
57+
configs.add(ReactiveConfigInfo(clazz, reactiveConfig))
58+
}
59+
1660
fun buildNode(path: String?): Pair<CommentedConfigurationNode, YamlConfigurationLoader> {
17-
val file = File(if(path != null)"${dirPath}/${path.lowercase()}.yml" else dirPath)
18-
if(!file.exists()) {
61+
val file = File(if (path != null) "${dirPath}/${path.lowercase()}.yml" else dirPath)
62+
if (!file.exists()) {
1963
file.parentFile.mkdirs()
2064
file.createNewFile()
2165
}
2266
val loader = YamlConfigurationLoader.builder()
2367
.path(file.toPath())
2468
.nodeStyle(NodeStyle.BLOCK)
2569
.defaultOptions { options ->
26-
options.serializers{ builder ->
70+
options.serializers { builder ->
2771
builder.registerAnnotatedObjects(objectMapperFactory())
2872
}
2973
}.build()
3074
return Pair(loader.load(), loader)
3175
}
3276

33-
inline fun <reified T> load(path: String?) : T? {
34-
val node = buildNode(path).first
35-
return node.get<T>()
36-
}
37-
3877
fun <T> save(obj: T) {
3978
save(null, obj)
4079
}
@@ -43,5 +82,90 @@ open class YamlConfig(val dirPath: String) {
4382
val pair = buildNode(path)
4483
pair.first.set(obj)
4584
pair.second.save(pair.first)
85+
86+
// Update cache after successful save
87+
val cacheKey = path ?: "default"
88+
if (obj != null) {
89+
configCache[cacheKey] = obj
90+
}
91+
}
92+
93+
fun <T> save(path: String?, reactiveConfig: ReactiveConfig<T>) {
94+
return save(path, reactiveConfig.get())
95+
}
96+
97+
private fun startWatcher() {
98+
val directory = File(dirPath).toPath()
99+
if (!directory.toFile().exists()) {
100+
directory.toFile().mkdirs()
101+
}
102+
103+
try {
104+
directory.register(
105+
watchService,
106+
StandardWatchEventKinds.ENTRY_MODIFY
107+
)
108+
109+
watcherJob = CoroutineScope(Dispatchers.IO).launch {
110+
while (isActive) {
111+
try {
112+
val key = watchService.take()
113+
for (event in key.pollEvents()) {
114+
val path = event.context() as? Path ?: continue
115+
val resolvedPath = directory.resolve(path)
116+
117+
if (Files.isDirectory(resolvedPath) || !resolvedPath.toString().endsWith(".yml")) {
118+
continue
119+
}
120+
121+
val kind = event.kind()
122+
if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
123+
handleFileChange(resolvedPath.toFile())
124+
}
125+
}
126+
key.reset()
127+
} catch (e: Exception) {
128+
println("Error in config watcher: ${e.message}")
129+
}
130+
}
131+
}
132+
} catch (e: Exception) {
133+
println("Could not start config file watcher: ${e.message}")
134+
}
135+
}
136+
137+
private fun handleFileChange(file: File) {
138+
val fileName = file.nameWithoutExtension
139+
val cacheKey = if (file.name == File(dirPath).name) "default" else fileName
140+
141+
val reactiveConfigList = reactiveConfigs[cacheKey] ?: return
142+
143+
reactiveConfigList.forEach { configInfo ->
144+
try {
145+
val configPath = if (cacheKey == "default") null else fileName
146+
147+
@Suppress("UNCHECKED_CAST")
148+
val typedConfigInfo = configInfo as ReactiveConfigInfo<Any>
149+
val newValue = loadDirect(configPath, typedConfigInfo.clazz)
150+
151+
typedConfigInfo.reactiveConfig.update(newValue)
152+
153+
} catch (ex: ParsingException) {
154+
println("Config file ${file.name} has parsing errors. Keeping old version.")
155+
// Don't update the reactive config, keep the old cached version
156+
} catch (e: Exception) {
157+
println("Error updating reactive config: ${e.message}")
158+
}
159+
}
46160
}
47-
}
161+
162+
fun close() {
163+
watcherJob?.cancel()
164+
try {
165+
watchService.close()
166+
} catch (e: Exception) {
167+
println("Error closing watch service: ${e.message}")
168+
}
169+
}
170+
171+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package app.simplecloud.plugin.proxy.shared.config.reactive
2+
3+
import app.simplecloud.plugin.proxy.shared.config.YamlConfig
4+
5+
class ReactiveConfig<T>(
6+
config: YamlConfig,
7+
path: String?,
8+
clazz: Class<T>
9+
) {
10+
11+
@Volatile
12+
private var currentValue: T? = config.loadDirect(path, clazz)
13+
14+
init {
15+
config.registerReactiveConfig(path, clazz, this)
16+
}
17+
18+
fun get(): T = currentValue?: throw NullPointerException("Reactive config is not initialized")
19+
20+
internal fun update(newValue: T?) {
21+
currentValue = newValue
22+
}
23+
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package app.simplecloud.plugin.proxy.shared.config.reactive
2+
3+
4+
data class ReactiveConfigInfo<T>(
5+
val clazz: Class<T>,
6+
val reactiveConfig: ReactiveConfig<T>
7+
)

proxy-shared/src/main/kotlin/app/simplecloud/plugin/proxy/shared/handler/JoinStateHandler.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class JoinStateHandler(
1212

1313
private val logger = Logger.getLogger(JoinStateHandler::class.java.name)
1414

15-
var localState: String = proxyPlugin.joinStateConfiguration.defaultState
15+
var localState: String = proxyPlugin.joinStateConfiguration.get().defaultState
1616

1717
companion object {
1818
val JOINSTATE_KEY = "joinstate"
@@ -39,7 +39,7 @@ class JoinStateHandler(
3939

4040
if (groupProperties.isEmpty()) {
4141
logger.warning("No join state found for group $groupName. Using default join state.")
42-
setJoinStateAtGroup(groupName, this.proxyPlugin.joinStateConfiguration.defaultState)
42+
setJoinStateAtGroup(groupName, this.proxyPlugin.joinStateConfiguration.get().defaultState)
4343

4444
return getJoinStateAtGroup(groupName)
4545
}
@@ -104,7 +104,7 @@ class JoinStateHandler(
104104

105105
if (serviceProperties.isEmpty()) {
106106
logger.warning("No join state found for service $numericalId in group $groupName. Using default join state.")
107-
setJoinStateAtService(groupName, numericalId, this.proxyPlugin.joinStateConfiguration.defaultState)
107+
setJoinStateAtService(groupName, numericalId, this.proxyPlugin.joinStateConfiguration.get().defaultState)
108108

109109
return getJoinStateAtService(groupName, numericalId)
110110
}
@@ -134,9 +134,9 @@ class JoinStateHandler(
134134
CoroutineScope(Dispatchers.IO).launch {
135135
setJoinStateAtGroupAndAllServicesInGroup(
136136
event.serverAfter.groupName,
137-
proxyPlugin.joinStateConfiguration.defaultState
137+
proxyPlugin.joinStateConfiguration.get().defaultState
138138
)
139-
localState = proxyPlugin.joinStateConfiguration.defaultState
139+
localState = proxyPlugin.joinStateConfiguration.get().defaultState
140140
}
141141
return@subscribe
142142
}
@@ -164,7 +164,7 @@ class JoinStateHandler(
164164
logger.warning("No join state found for group ${cloudControllerHandler.groupName}. Using default join state.")
165165
setJoinStateAtGroup(
166166
cloudControllerHandler.groupName!!,
167-
this.proxyPlugin.joinStateConfiguration.defaultState
167+
this.proxyPlugin.joinStateConfiguration.get().defaultState
168168
)
169169
return
170170
}

0 commit comments

Comments
 (0)