feat: middle save

This commit is contained in:
Project_IO 2024-10-02 12:49:00 +09:00
parent a360e53d0f
commit e9f17fcd78
8 changed files with 238 additions and 197 deletions

View file

@ -2,7 +2,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
group=net.projecttl group=net.projecttl
version=0.2.3-SNAPSHOT version=0.3.0-SNAPSHOT
ktor_version=2.3.12 ktor_version=2.3.12
log4j_version=2.23.1 log4j_version=2.23.1

View file

@ -0,0 +1,70 @@
package net.projecttl.p.x32.api
import org.jetbrains.exposed.sql.Database
import java.io.File
import java.io.FileInputStream
import java.util.*
import kotlin.reflect.KProperty
import kotlin.system.exitProcess
object BotConfig {
private fun useBotConfig(): BotConfigDelegate {
return BotConfigDelegate()
}
fun useDatabase(): Database {
if (db_username.isNotBlank() && db_password.isNotBlank()) {
return Database.connect(
driver = db_driver,
url = db_url,
user = db_username,
password = db_password
)
}
return Database.connect(
driver = db_driver,
url = db_url
)
}
val token: String by useBotConfig()
val owner: String by useBotConfig()
private val bundle_func: String by useBotConfig()
val bundle = when (bundle_func) {
"1" -> true
"0" -> false
else -> {
throw IllegalArgumentException("bundle_func option must be 0 or 1")
}
}
private val db_driver: String by useBotConfig()
private val db_url: String by useBotConfig()
private val db_username: String by useBotConfig()
private val db_password: String by useBotConfig()
}
private class BotConfigDelegate {
private val props = Properties()
init {
val file = File("config.properties")
if (!file.exists()) {
val default = this.javaClass.getResourceAsStream("/config.sample.properties")!!.readBytes()
file.outputStream().use { stream ->
stream.write(default)
}
println("config.properties is not found, create new one...")
exitProcess(1)
}
props.load(FileInputStream(file))
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return props.getProperty(property.name).toString()
}
}

View file

@ -7,28 +7,28 @@ import net.dv8tion.jda.api.JDA
import net.projecttl.p.x32.command.Info import net.projecttl.p.x32.command.Info
import net.projecttl.p.x32.command.PluginCommand import net.projecttl.p.x32.command.PluginCommand
import net.projecttl.p.x32.command.Reload import net.projecttl.p.x32.command.Reload
import net.projecttl.p.x32.config.Config import net.projecttl.p.x32.api.BotConfig
import net.projecttl.p.x32.config.DefaultConfig import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.kernel.CoreKernel import net.projecttl.p.x32.kernel.CoreKernel
import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
lateinit var jda: JDA lateinit var jda: JDA
private set
lateinit var kernel: CoreKernel lateinit var kernel: CoreKernel
lateinit var database: Database private set
val logger: Logger = LoggerFactory.getLogger(Px32::class.java) val logger: Logger = LoggerFactory.getLogger(Px32::class.java)
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun main() { fun main() {
println("Px32 version v${DefaultConfig.version}") println("Px32 version v${DefaultConfig.version}")
if (Config.owner.isBlank() || Config.owner.isEmpty()) { if (BotConfig.owner.isBlank() || BotConfig.owner.isEmpty()) {
logger.warn("owner option is blank or empty!") logger.warn("owner option is blank or empty!")
} }
kernel = CoreKernel(Config.token) kernel = CoreKernel(BotConfig.token)
val handler = kernel.getCommandContainer() val handler = kernel.commandContainer
handler.addCommand(Info) handler.addCommand(Info)
handler.addCommand(Reload) handler.addCommand(Reload)

View file

@ -6,7 +6,7 @@ import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.dv8tion.jda.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.config.DefaultConfig import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader import net.projecttl.p.x32.kernel
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
object Info : GlobalCommand { object Info : GlobalCommand {
@ -18,7 +18,8 @@ object Info : GlobalCommand {
val rb = ManagementFactory.getRuntimeMXBean() val rb = ManagementFactory.getRuntimeMXBean()
val r = Runtime.getRuntime() val r = Runtime.getRuntime()
val size = PluginLoader.getPlugins().size val size = kernel.plugins.size
val hSize = kernel.handlers.size
val info = """ val info = """
Px32Bot v${DefaultConfig.version}, JDA `v${JDAInfo.VERSION}`, Px32Bot v${DefaultConfig.version}, JDA `v${JDAInfo.VERSION}`,
@ -30,7 +31,8 @@ object Info : GlobalCommand {
Assigned `${r.maxMemory() / 1048576}MB` of Max Memories at this Bot Assigned `${r.maxMemory() / 1048576}MB` of Max Memories at this Bot
Using `${(r.totalMemory() - r.freeMemory()) / 1048576}MB` at this Bot Using `${(r.totalMemory() - r.freeMemory()) / 1048576}MB` at this Bot
Total $size plugin${if (size > 1) "s" else ""} loaded Total $size plugin${if (size > 1) "s" else ""} loaded
Total $hSize handler${if (hSize > 1) "s" else ""} used
""".trimIndent() """.trimIndent()
ev.reply(info).queue() ev.reply(info).queue()

View file

@ -8,10 +8,10 @@ import net.dv8tion.jda.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.api.util.colour import net.projecttl.p.x32.api.util.colour
import net.projecttl.p.x32.api.util.footer import net.projecttl.p.x32.api.util.footer
import net.projecttl.p.x32.kernel.CoreKernel.PluginLoader import net.projecttl.p.x32.kernel
object PluginCommand : GlobalCommand { object PluginCommand : GlobalCommand {
override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData()) override val data = CommandData.fromData(CommandDataImpl("plugins", "봇에 불러온 플러그인을 확인 합니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) { override suspend fun execute(ev: SlashCommandInteractionEvent) {
val embed = EmbedBuilder().apply { val embed = EmbedBuilder().apply {
@ -22,7 +22,7 @@ object PluginCommand : GlobalCommand {
footer(ev.user) footer(ev.user)
} }
val loader = PluginLoader.getPlugins() val loader = kernel.plugins
val fields = loader.map { (c, _) -> val fields = loader.map { (c, _) ->
MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true) MessageEmbed.Field(":electric_plug: **${c.name}**", "`${c.version}`", true)
} }

View file

@ -4,7 +4,7 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEve
import net.dv8tion.jda.api.interactions.commands.build.CommandData import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.internal.interactions.CommandDataImpl import net.dv8tion.jda.internal.interactions.CommandDataImpl
import net.projecttl.p.x32.api.command.GlobalCommand import net.projecttl.p.x32.api.command.GlobalCommand
import net.projecttl.p.x32.config.Config import net.projecttl.p.x32.api.BotConfig
import net.projecttl.p.x32.kernel import net.projecttl.p.x32.kernel
object Reload : GlobalCommand { object Reload : GlobalCommand {
@ -15,7 +15,7 @@ object Reload : GlobalCommand {
return return
} }
if (ev.user.id != Config.owner) { if (ev.user.id != BotConfig.owner) {
return ev.reply(":warning: 권한을 가지고 있지 않아요").queue() return ev.reply(":warning: 권한을 가지고 있지 않아요").queue()
} }

View file

@ -1,48 +0,0 @@
package net.projecttl.p.x32.config
import net.projecttl.p.x32.logger
import java.io.File
import java.io.FileInputStream
import java.util.*
import kotlin.reflect.KProperty
import kotlin.system.exitProcess
object Config {
private fun useConfig(): ConfigDelegate {
return ConfigDelegate()
}
val token: String by useConfig()
val owner: String by useConfig()
private val bundle_func: String by useConfig()
val bundle = if (bundle_func == "1") true else if (bundle_func == "0") false else throw IllegalArgumentException("bundle_func option must be 0 or 1")
val db_driver: String by useConfig()
val db_url: String by useConfig()
val db_username: String by useConfig()
val db_password: String by useConfig()
}
private class ConfigDelegate {
private val props = Properties()
init {
val file = File("config.properties")
if (!file.exists()) {
val default = this.javaClass.getResourceAsStream("/config.sample.properties")!!.readBytes()
file.outputStream().use { stream ->
stream.write(default)
}
logger.error("config.properties is not found, create new one...")
exitProcess(1)
}
props.load(FileInputStream(file))
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return props.getProperty(property.name).toString()
}
}

View file

@ -7,10 +7,11 @@ import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.requests.GatewayIntent import net.dv8tion.jda.api.requests.GatewayIntent
import net.dv8tion.jda.api.utils.MemberCachePolicy import net.dv8tion.jda.api.utils.MemberCachePolicy
import net.projecttl.p.x32.api.BotConfig
import net.projecttl.p.x32.api.Plugin import net.projecttl.p.x32.api.Plugin
import net.projecttl.p.x32.api.command.CommandHandler import net.projecttl.p.x32.api.command.CommandHandler
import net.projecttl.p.x32.api.model.PluginConfig import net.projecttl.p.x32.api.model.PluginConfig
import net.projecttl.p.x32.config.Config import net.projecttl.p.x32.config.DefaultConfig
import net.projecttl.p.x32.func.BundleModule import net.projecttl.p.x32.func.BundleModule
import net.projecttl.p.x32.logger import net.projecttl.p.x32.logger
import java.io.File import java.io.File
@ -19,6 +20,9 @@ import java.nio.charset.Charset
import java.util.jar.JarFile import java.util.jar.JarFile
class CoreKernel(token: String) { class CoreKernel(token: String) {
lateinit var jda: JDA
private set
private val builder = JDABuilder.createDefault(token, listOf( private val builder = JDABuilder.createDefault(token, listOf(
GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_PRESENCES,
GatewayIntent.GUILD_MEMBERS, GatewayIntent.GUILD_MEMBERS,
@ -28,189 +32,202 @@ class CoreKernel(token: String) {
GatewayIntent.GUILD_EMOJIS_AND_STICKERS, GatewayIntent.GUILD_EMOJIS_AND_STICKERS,
GatewayIntent.SCHEDULED_EVENTS GatewayIntent.SCHEDULED_EVENTS
)).setMemberCachePolicy(MemberCachePolicy.ALL) )).setMemberCachePolicy(MemberCachePolicy.ALL)
private val handlers = mutableListOf<ListenerAdapter>()
private val commandContainer = CommandHandler()
val memLock = Mutex() val memLock = Mutex()
val plugins get() = PluginLoader.getPlugins().map { it.value } val commandContainer = CommandHandler()
val plugins = mutableMapOf<PluginConfig, Plugin>()
var isActive = false
private set
private fun include() { private val parentDir = File("./plugins").apply {
if (Config.bundle) { if (!exists()) {
val b = BundleModule() mkdirs()
PluginLoader.putModule(b.config, b)
} }
} }
fun getCommandContainer(): CommandHandler { val handlers: List<ListenerAdapter>
return commandContainer get() {
if (!isActive) {
return listOf()
}
return jda.eventManager.registeredListeners.map { it as ListenerAdapter }
} }
fun addHandler(handler: ListenerAdapter) { fun addHandler(handler: ListenerAdapter) {
handlers.add(handler) if (isActive) {
jda.addEventListener(handler)
return
}
builder.addEventListeners(handler)
} }
fun delHandler(handler: ListenerAdapter) { fun delHandler(handler: ListenerAdapter) {
handlers.remove(handler) if (isActive) {
} jda.removeEventListener(handler)
return
fun build(): JDA {
include()
PluginLoader.load()
plugins.forEach { plugin ->
plugin.handlers.forEach { handler ->
handlers.add(handler)
}
} }
handlers.map { builder.removeEventListeners(handler)
logger.info("Load event listener: ${it::class.simpleName}")
builder.addEventListeners(it)
}
builder.addEventListeners(commandContainer)
return builder.build()
} }
fun register(jda: JDA) { fun register(jda: JDA) {
commandContainer.register(jda) commandContainer.register(jda)
handlers.forEach { h -> jda.eventManager.registeredListeners.filterIsInstance<CommandHandler>().forEach { h ->
if (h is CommandHandler) { h.register(jda)
h.register(jda)
}
} }
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {
PluginLoader.destroy() destroy()
}) })
} }
private fun include() {
if (BotConfig.bundle) {
val b = BundleModule()
loadModule(b.config, b)
}
}
private fun load() {
parentDir.listFiles()?.forEach { file ->
try {
loadPlugin(file)
} catch (ex: Exception) {
logger.error("error occurred while to plugin loading: ${ex.message}")
}
}
logger.info("Loaded ${plugins.size} plugins")
}
private fun destroy() {
val unloaded = mutableListOf<PluginConfig>()
plugins.forEach { (config, plugin) ->
logger.info("disable ${config.name} plugin...")
try {
plugin.destroy()
} catch (ex: Exception) {
logger.error("failed to destroy ${config.name} plugin")
ex.printStackTrace()
}
}
unloaded.forEach {
plugins.remove(it)
}
}
suspend fun reload(jda: JDA) { suspend fun reload(jda: JDA) {
if (!memLock.isLocked) { if (!memLock.isLocked) {
memLock.lock() memLock.lock()
} }
PluginLoader.destroy() plugins.forEach { (_, plugin) ->
plugins.forEach { plugin -> plugin.handlers.forEach {
plugin.handlers.filter { handlers.contains(it) }.map { delHandler(it)
handlers.remove(it)
} }
} }
include() destroy()
PluginLoader.load() logger.info(jda.eventManager.registeredListeners.size.toString())
plugins.forEach { plugin -> load()
plugin.handlers.forEach { handler -> logger.info(jda.eventManager.registeredListeners.size.toString())
if (!handlers.contains(handler)) {
handlers.add(handler) plugins.forEach { (_, plugin) ->
jda.addEventListener(handler) plugin.handlers.forEach {
} addHandler(it)
} }
} }
handlers.forEach { h -> logger.info(jda.eventManager.registeredListeners.size.toString())
if (h is CommandHandler) {
h.register(jda) handlers.filterIsInstance<CommandHandler>().forEach { h ->
} h.register(jda)
} }
memLock.unlock() memLock.unlock()
} }
object PluginLoader { private fun loadPlugin(file: File) {
private val plugins = mutableMapOf<PluginConfig, Plugin>() if (file.name == "px32-bot-module") {
private val parentDir = File("./plugins").apply { return
if (!exists()) {
mkdirs()
}
} }
fun getPlugins(): Map<PluginConfig, Plugin> { if (!file.name.endsWith(".jar")) {
return plugins.toMap() return
} }
fun putModule(config: PluginConfig, plugin: Plugin) { val jar = JarFile(file)
try { val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" }
logger.info("Load module ${config.name} v${config.version}") if (cnf == null)
plugin.onLoad() throw IllegalAccessException("${file.name} is not a plugin. aborted")
} catch (ex: Exception) {
ex.printStackTrace()
plugin.destroy()
return
}
plugins[config] = plugin val stream = jar.getInputStream(cnf)
val raw = stream.use {
return@use it.readBytes().toString(Charset.forName("UTF-8"))
} }
fun load() { val config = Json.decodeFromString<PluginConfig>(raw)
parentDir.listFiles()?.forEach { file -> val cl = URLClassLoader(arrayOf(file.toPath().toUri().toURL()))
try { val obj = cl.loadClass(config.main).getDeclaredConstructor().newInstance()
loadPlugin(file)
} catch (ex: Exception) {
logger.error("error occurred while to plugin loading: ${ex.message}")
}
}
logger.info("Loaded ${plugins.size} plugins") if (obj !is Plugin)
} throw IllegalAccessException("${config.name} is not valid plugin class. aborted")
fun destroy() { try {
val unloaded = mutableListOf<PluginConfig>() loadModule(config, obj)
} catch (ex: Exception) {
plugins.forEach { (config, plugin) -> throw ex
logger.info("disable ${config.name} plugin...")
try {
plugin.destroy()
} catch (ex: Exception) {
logger.error("failed to destroy ${config.name} plugin")
ex.printStackTrace()
}
unloaded += config
}
unloaded.forEach {
plugins.remove(it)
}
}
private fun loadPlugin(file: File) {
if (file.name == "px32-bot-module") {
return
}
if (!file.name.endsWith(".jar")) {
return
}
val jar = JarFile(file)
val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" }
if (cnf == null)
return logger.error("${file.name} is not a plugin. aborted")
val stream = jar.getInputStream(cnf)
val raw = stream.use {
return@use it.readBytes().toString(Charset.forName("UTF-8"))
}
val config = Json.decodeFromString<PluginConfig>(raw)
val cl = URLClassLoader(arrayOf(file.toPath().toUri().toURL()))
val obj = cl.loadClass(config.main).getDeclaredConstructor().newInstance()
if (obj !is Plugin)
return logger.error("${config.name} is not valid main class. aborted")
try {
logger.info("Load plugin ${config.name} v${config.version}")
obj.onLoad()
} catch (ex: Exception) {
ex.printStackTrace()
return logger.error("Failed to load plugin ${config.name}")
}
plugins[config] = obj
} }
} }
private fun loadModule(config: PluginConfig, plugin: Plugin) {
try {
logger.info("Load plugin ${config.name} v${config.version}")
plugin.onLoad()
} catch (ex: Exception) {
try {
plugin.destroy()
} catch (ex: Exception) {
throw ex
}
throw ex
}
plugins[config] = plugin
}
fun build(): JDA {
if (isActive) {
logger.error("core kernel is already loaded! you cannot rebuild this kernel.")
return jda
}
include()
plugins.forEach { (_, plugin) ->
plugin.handlers.forEach { handler ->
logger.info("Load event listener: ${handler::class.simpleName}")
addHandler(handler)
}
}
builder.addEventListeners(commandContainer)
jda = builder.build()
isActive = true
Runtime.getRuntime().addShutdownHook(Thread {
isActive = false
logger.info("shutdown now Px32 kernel v${DefaultConfig.version}")
jda.shutdownNow()
})
return jda
}
} }