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
group=net.projecttl
version=0.2.3-SNAPSHOT
version=0.3.0-SNAPSHOT
ktor_version=2.3.12
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.PluginCommand
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.kernel.CoreKernel
import org.jetbrains.exposed.sql.Database
import org.slf4j.Logger
import org.slf4j.LoggerFactory
lateinit var jda: JDA
private set
lateinit var kernel: CoreKernel
lateinit var database: Database
private set
val logger: Logger = LoggerFactory.getLogger(Px32::class.java)
@OptIn(DelicateCoroutinesApi::class)
fun main() {
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!")
}
kernel = CoreKernel(Config.token)
val handler = kernel.getCommandContainer()
kernel = CoreKernel(BotConfig.token)
val handler = kernel.commandContainer
handler.addCommand(Info)
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.projecttl.p.x32.api.command.GlobalCommand
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
object Info : GlobalCommand {
@ -18,7 +18,8 @@ object Info : GlobalCommand {
val rb = ManagementFactory.getRuntimeMXBean()
val r = Runtime.getRuntime()
val size = PluginLoader.getPlugins().size
val size = kernel.plugins.size
val hSize = kernel.handlers.size
val info = """
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
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()
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.util.colour
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 {
override val data = CommandData.fromData(CommandDataImpl("plugin", "봇에 불러온 플러그인을 확인 합니다").toData())
override val data = CommandData.fromData(CommandDataImpl("plugins", "봇에 불러온 플러그인을 확인 합니다").toData())
override suspend fun execute(ev: SlashCommandInteractionEvent) {
val embed = EmbedBuilder().apply {
@ -22,7 +22,7 @@ object PluginCommand : GlobalCommand {
footer(ev.user)
}
val loader = PluginLoader.getPlugins()
val loader = kernel.plugins
val fields = loader.map { (c, _) ->
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.internal.interactions.CommandDataImpl
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
object Reload : GlobalCommand {
@ -15,7 +15,7 @@ object Reload : GlobalCommand {
return
}
if (ev.user.id != Config.owner) {
if (ev.user.id != BotConfig.owner) {
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.requests.GatewayIntent
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.command.CommandHandler
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.logger
import java.io.File
@ -19,6 +20,9 @@ import java.nio.charset.Charset
import java.util.jar.JarFile
class CoreKernel(token: String) {
lateinit var jda: JDA
private set
private val builder = JDABuilder.createDefault(token, listOf(
GatewayIntent.GUILD_PRESENCES,
GatewayIntent.GUILD_MEMBERS,
@ -28,189 +32,202 @@ class CoreKernel(token: String) {
GatewayIntent.GUILD_EMOJIS_AND_STICKERS,
GatewayIntent.SCHEDULED_EVENTS
)).setMemberCachePolicy(MemberCachePolicy.ALL)
private val handlers = mutableListOf<ListenerAdapter>()
private val commandContainer = CommandHandler()
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() {
if (Config.bundle) {
val b = BundleModule()
PluginLoader.putModule(b.config, b)
private val parentDir = File("./plugins").apply {
if (!exists()) {
mkdirs()
}
}
fun getCommandContainer(): CommandHandler {
return commandContainer
val handlers: List<ListenerAdapter>
get() {
if (!isActive) {
return listOf()
}
return jda.eventManager.registeredListeners.map { it as ListenerAdapter }
}
fun addHandler(handler: ListenerAdapter) {
handlers.add(handler)
if (isActive) {
jda.addEventListener(handler)
return
}
builder.addEventListeners(handler)
}
fun delHandler(handler: ListenerAdapter) {
handlers.remove(handler)
}
fun build(): JDA {
include()
PluginLoader.load()
plugins.forEach { plugin ->
plugin.handlers.forEach { handler ->
handlers.add(handler)
}
if (isActive) {
jda.removeEventListener(handler)
return
}
handlers.map {
logger.info("Load event listener: ${it::class.simpleName}")
builder.addEventListeners(it)
}
builder.addEventListeners(commandContainer)
return builder.build()
builder.removeEventListeners(handler)
}
fun register(jda: JDA) {
commandContainer.register(jda)
handlers.forEach { h ->
if (h is CommandHandler) {
h.register(jda)
}
jda.eventManager.registeredListeners.filterIsInstance<CommandHandler>().forEach { h ->
h.register(jda)
}
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) {
if (!memLock.isLocked) {
memLock.lock()
}
PluginLoader.destroy()
plugins.forEach { plugin ->
plugin.handlers.filter { handlers.contains(it) }.map {
handlers.remove(it)
plugins.forEach { (_, plugin) ->
plugin.handlers.forEach {
delHandler(it)
}
}
include()
PluginLoader.load()
destroy()
logger.info(jda.eventManager.registeredListeners.size.toString())
plugins.forEach { plugin ->
plugin.handlers.forEach { handler ->
if (!handlers.contains(handler)) {
handlers.add(handler)
jda.addEventListener(handler)
}
load()
logger.info(jda.eventManager.registeredListeners.size.toString())
plugins.forEach { (_, plugin) ->
plugin.handlers.forEach {
addHandler(it)
}
}
handlers.forEach { h ->
if (h is CommandHandler) {
h.register(jda)
}
logger.info(jda.eventManager.registeredListeners.size.toString())
handlers.filterIsInstance<CommandHandler>().forEach { h ->
h.register(jda)
}
memLock.unlock()
}
object PluginLoader {
private val plugins = mutableMapOf<PluginConfig, Plugin>()
private val parentDir = File("./plugins").apply {
if (!exists()) {
mkdirs()
}
private fun loadPlugin(file: File) {
if (file.name == "px32-bot-module") {
return
}
fun getPlugins(): Map<PluginConfig, Plugin> {
return plugins.toMap()
if (!file.name.endsWith(".jar")) {
return
}
fun putModule(config: PluginConfig, plugin: Plugin) {
try {
logger.info("Load module ${config.name} v${config.version}")
plugin.onLoad()
} catch (ex: Exception) {
ex.printStackTrace()
plugin.destroy()
return
}
val jar = JarFile(file)
val cnf = jar.entries().toList().singleOrNull { jarEntry -> jarEntry.name == "plugin.json" }
if (cnf == null)
throw IllegalAccessException("${file.name} is not a plugin. aborted")
plugins[config] = plugin
val stream = jar.getInputStream(cnf)
val raw = stream.use {
return@use it.readBytes().toString(Charset.forName("UTF-8"))
}
fun load() {
parentDir.listFiles()?.forEach { file ->
try {
loadPlugin(file)
} catch (ex: Exception) {
logger.error("error occurred while to plugin loading: ${ex.message}")
}
}
val config = Json.decodeFromString<PluginConfig>(raw)
val cl = URLClassLoader(arrayOf(file.toPath().toUri().toURL()))
val obj = cl.loadClass(config.main).getDeclaredConstructor().newInstance()
logger.info("Loaded ${plugins.size} plugins")
}
if (obj !is Plugin)
throw IllegalAccessException("${config.name} is not valid plugin class. aborted")
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 += 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
try {
loadModule(config, obj)
} catch (ex: Exception) {
throw ex
}
}
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
}
}