This commit is contained in:
2023-05-04 21:18:04 +08:00
parent 4b65a2459b
commit f971211e33
34 changed files with 90 additions and 87 deletions
+28
View File
@@ -0,0 +1,28 @@
plugins {
kotlin("jvm")
id("com.github.johnrengelman.shadow")
}
group = "cc.maxmc.msm.controlled"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-Beta")
implementation(project(":common"))
compileOnly("io.netty:netty-all:4.1.90.Final")
@Suppress("VulnerableLibrariesLocal") compileOnly("io.github.waterfallmc:waterfall-api:1.19-R0.1-SNAPSHOT")
}
tasks.shadowJar {
archiveClassifier.set(null as? String?)
relocate("kotlin", "cc.maxmc.msm.lib.kotlin")
}
tasks.build {
dependsOn(tasks.shadowJar)
}
@@ -0,0 +1,39 @@
package cc.maxmc.msm.controlled
import cc.maxmc.msm.api.MultiServerManAPIProvider
import cc.maxmc.msm.controlled.api.APIImpl
import cc.maxmc.msm.controlled.listener.APIPacketListener
import cc.maxmc.msm.controlled.command.Api
import cc.maxmc.msm.controlled.command.Send
import cc.maxmc.msm.controlled.listener.ProtocolListener
import cc.maxmc.msm.controlled.netty.NetClient
import cc.maxmc.msm.controlled.settings.Settings
import cc.maxmc.msm.controlled.settings.SettingsReader
import cc.maxmc.msm.common.network.netty.NetworkRegistry
import net.md_5.bungee.api.ProxyServer
import net.md_5.bungee.api.plugin.Plugin
class MultiServerMan : Plugin() {
init {
instance = this
}
override fun onEnable() {
ProxyServer.getInstance().pluginManager.registerCommand(this, Send)
ProxyServer.getInstance().pluginManager.registerCommand(this, Api)
ProxyServer.getInstance().pluginManager.registerListener(this, APIPacketListener)
ProxyServer.getInstance().pluginManager.registerListener(this, ProtocolListener)
SettingsReader
NetworkRegistry
NetClient.init(Settings.Parent.address, Settings.Parent.port)
MultiServerManAPIProvider.register(APIImpl)
}
override fun onDisable() {
NetClient.shutdown()
}
companion object {
lateinit var instance: MultiServerMan
}
}
@@ -0,0 +1,48 @@
package cc.maxmc.msm.controlled.api
import cc.maxmc.msm.api.MultiServerManAPI
import cc.maxmc.msm.api.misc.MatchInfo
import cc.maxmc.msm.controlled.netty.NetClient
import cc.maxmc.msm.common.network.packet.PPacketAPICall
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@Suppress("UNCHECKED_CAST")
object APIImpl : MultiServerManAPI {
val apiCallCache = HashMap<UUID, CompletableFuture<in Any>>()
private const val TIMEOUT = 3L
override fun getServer(type: String, players: MutableList<String>): MatchInfo {
val packet = PPacketAPICall.PPacketCallGetServer(type, players)
NetClient.sendPacket(packet)
val future = CompletableFuture<MatchInfo>()
apiCallCache[packet.uid] = future as CompletableFuture<Any>
return future.get(TIMEOUT, TimeUnit.SECONDS)
}
override fun informEnd(id: Int) {
val packet = PPacketAPICall.PPacketCallInformEnd(id)
NetClient.sendPacket(packet)
val future = CompletableFuture<Unit>()
apiCallCache[packet.uid] = future as CompletableFuture<Any>
future.get(TIMEOUT, TimeUnit.SECONDS)
return
}
override fun getPlayerServer(player: String): MatchInfo {
val packet = PPacketAPICall.PPacketCallGetPlayerServer(player)
NetClient.sendPacket(packet)
val future = CompletableFuture<MatchInfo>()
apiCallCache[packet.uid] = future as CompletableFuture<Any>
return future.get(TIMEOUT, TimeUnit.SECONDS)
}
override fun containPlayer(player: String): Boolean {
val packet = PPacketAPICall.PPacketCallContainPlayer(player)
NetClient.sendPacket(packet)
val future = CompletableFuture<Boolean>()
apiCallCache[packet.uid] = future as CompletableFuture<Any>
return future.get(TIMEOUT, TimeUnit.SECONDS)
}
}
@@ -0,0 +1,36 @@
package cc.maxmc.msm.controlled.command
import cc.maxmc.msm.api.MultiServerManAPIProvider
import net.md_5.bungee.api.CommandSender
import net.md_5.bungee.api.chat.TextComponent
import net.md_5.bungee.api.plugin.Command
object Api : Command("api") {
override fun execute(sender: CommandSender, args: Array<out String>) {
sender.sendMessage(TextComponent("Calling API"))
val api = args[0]
val apiInst = MultiServerManAPIProvider.getAPI()
@Suppress("IMPLICIT_CAST_TO_ANY") val ret = when (api) {
"gs" -> {
apiInst.getServer("def", listOf("abc", "def"))
}
"ie" -> {
apiInst.informEnd(100)
}
"gps" -> {
apiInst.getPlayerServer("def")
}
"cp" -> {
apiInst.containPlayer("awa")
}
else -> {
"non api exist"
}
}.toString()
sender.sendMessage(TextComponent("Returning $ret"))
}
}
@@ -0,0 +1,16 @@
package cc.maxmc.msm.controlled.command
import cc.maxmc.msm.controlled.netty.NetClient
import cc.maxmc.msm.common.network.packet.PPacketDebug
import net.md_5.bungee.api.CommandSender
import net.md_5.bungee.api.chat.TextComponent
import net.md_5.bungee.api.plugin.Command
object Send : Command("debugP") {
override fun execute(sender: CommandSender, args: Array<out String>) {
val content = args.joinToString(" ")
val packet = PPacketDebug(content)
NetClient.sendPacket(packet)
sender.sendMessage(TextComponent("Success."))
}
}
@@ -0,0 +1,40 @@
package cc.maxmc.msm.controlled.listener
import cc.maxmc.msm.controlled.api.APIImpl
import cc.maxmc.msm.common.event.PacketReceiveEvent
import cc.maxmc.msm.common.network.packet.CPacketAPICallback
import net.md_5.bungee.api.plugin.Listener
import net.md_5.bungee.event.EventHandler
object APIPacketListener : Listener {
@EventHandler
fun onPacket(packetEvent: PacketReceiveEvent) {
val packet = packetEvent.packet
if (packet !is CPacketAPICallback) {
return
}
val future = APIImpl.apiCallCache[packet.uid]
when (packet) {
is CPacketAPICallback.CPacketCallbackContainPlayer -> {
future?.complete(packet.value)
?: throw IllegalStateException("Packet callback received, however no request")
}
is CPacketAPICallback.CPacketCallbackGetPlayerServer -> {
future?.complete(packet.matchInfo)
?: throw IllegalStateException("Packet callback received, however no request")
}
is CPacketAPICallback.CPacketCallbackGetServer -> {
future?.complete(packet.matchInfo)
?: throw IllegalStateException("Packet callback received, however no request")
}
is CPacketAPICallback.CPacketCallbackInformEnd -> {
future?.complete(Unit)
?: throw IllegalStateException("Packet callback received, however no request")
}
}
}
}
@@ -0,0 +1,64 @@
package cc.maxmc.msm.controlled.listener
import cc.maxmc.msm.controlled.MultiServerMan
import cc.maxmc.msm.controlled.misc.SubServer
import cc.maxmc.msm.controlled.netty.NetClient
import cc.maxmc.msm.controlled.settings.Settings
import cc.maxmc.msm.common.event.ChannelInactiveEvent
import cc.maxmc.msm.common.event.PacketReceiveEvent
import cc.maxmc.msm.common.network.packet.*
import cc.maxmc.msm.common.utils.log
import cc.maxmc.msm.common.utils.pluginScope
import kotlinx.coroutines.launch
import net.md_5.bungee.api.plugin.Listener
import net.md_5.bungee.event.EventHandler
import java.util.*
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
object ProtocolListener : Listener {
private val serverCache = HashMap<UUID, SubServer>()
@EventHandler
fun onGetInfo(packetE: PacketReceiveEvent) {
if (packetE.packet !is CPacketGetInfo) return
val types = MultiServerMan.instance.dataFolder.toPath().resolve("patterns").listDirectoryEntries()
.filter { it.isDirectory() }.map { it.name }.toMutableSet()
val packetCallBack = PPacketChildInfo(Settings.portRange.toHashSet(), types)
NetClient.sendPacket(packetCallBack)
}
@EventHandler
fun onRequestServer(packetE: PacketReceiveEvent) {
val packet = packetE.packet as? CPacketRequestServer ?: return
val serverInfo = packet.serverInfo
log("§a| §7正在启动服务器 ${serverInfo.server}")
val subServer = SubServer(serverInfo.uid, packet.type, serverInfo.server!!.split(":")[1].toInt())
serverCache[serverInfo.uid] = subServer
pluginScope.launch {
try {
subServer.initServer()
subServer.startServer()
} catch (e: Exception) {
NetClient.sendPacket(PPacketServerStarted())
}
}
}
@EventHandler
fun onDisconnect(evt: ChannelInactiveEvent) {
log("§c| §7与远程服务器断开连接.")
}
@EventHandler
fun onServerEnd(evt: PacketReceiveEvent) {
val packet = evt.packet as? CPacketEndServer ?: return
val serverInfo = packet.server
val server =
serverCache[serverInfo.uid] ?: throw IllegalAccessError("Server ($serverInfo) is not in this sub bungee.")
pluginScope.launch {
server.cleanup()
}
}
}
@@ -0,0 +1,42 @@
package cc.maxmc.msm.controlled.misc
import cc.maxmc.msm.controlled.MultiServerMan
import cc.maxmc.msm.controlled.settings.Settings
import cc.maxmc.msm.controlled.utils.ScriptRunner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.io.path.copyToRecursively
import kotlin.io.path.createDirectories
import kotlin.io.path.deleteRecursively
@Suppress("CanBeParameter", "MemberVisibilityCanBePrivate") // API
@OptIn(kotlin.io.path.ExperimentalPathApi::class)
class SubServer(
val uid: UUID, val type: String, val port: Int, baseFolder: Path = MultiServerMan.instance.dataFolder.toPath()
) {
private val serverFolder =
baseFolder.resolve("cache").resolve(uid.toString().substring(0..7)).also { it.createDirectories() }
private val patternFolder = baseFolder.resolve("patterns").resolve(type).also { it.createDirectories() }
private val runner = ScriptRunner(serverFolder.resolve("start.sh"))
suspend fun initServer() = withContext(Dispatchers.IO) {
patternFolder.copyToRecursively(serverFolder, followLinks = false, overwrite = true)
}
suspend fun startServer() = suspendCoroutine {
runner.launch(Settings.Patterns.arguments(type).joinToString(" ").replace("\${port}", port.toString()))
runner.onOutput("For help, type \"help\" or \"?\"") {
it.resume(Unit)
}
}
suspend fun cleanup() {
runner.exec("stop")
runner.exit()
serverFolder.deleteRecursively()
}
}
@@ -0,0 +1,54 @@
package cc.maxmc.msm.controlled.netty
import cc.maxmc.msm.common.network.BungeePacket
import cc.maxmc.msm.common.network.netty.NetworkRegistry
import cc.maxmc.msm.common.utils.log
import cc.maxmc.msm.common.utils.pipelineInit
import io.netty.bootstrap.Bootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
object NetClient {
private val loop = NioEventLoopGroup()
private lateinit var channel: Channel
private lateinit var boot: Bootstrap
fun init(address: String, port: Int) {
boot = Bootstrap().channel(NioSocketChannel::class.java).group(loop)
.handler(pipelineInit(NetworkRegistry.PacketDirection.CHILD_BOUND)).option(ChannelOption.TCP_NODELAY, true)
.remoteAddress(address, port)
runBlocking {
connect(0)
}
}
private suspend fun connect(delay: Long = 3000) {
delay(delay)
log("§b| §7正在尝试连接主BC")
val future = boot.connect().await()
val result = future.cause()
if (result == null) {
channel = future.channel()
log("§a| §7成功连接到集群的主节点. (${future.channel().remoteAddress()})")
return
}
log("§c| §7未能连接到主BC,将在 §c3s §7后重新连接。(原因: ${result.message})")
withContext(Dispatchers.IO) {
connect()
}
}
fun sendPacket(packet: BungeePacket) {
channel.writeAndFlush(packet)
}
fun shutdown() {
loop.shutdownGracefully().sync()
}
}
@@ -0,0 +1,27 @@
package cc.maxmc.msm.controlled.settings
import cc.maxmc.msm.controlled.settings.SettingsReader.config
object Settings {
val name
get() = config
val portRange
get() = config.getStringList("ports").flatMap {
it.toIntOrNull()?.run { listOf(this) } ?: it.split("..").run { this[0].toInt()..this[1].toInt() }
}
object Parent {
val address: String
get() = config.getString("parent.address", "localhost")
val port: Int
get() = config.getInt("parent.port", 23333)
}
object Patterns {
fun arguments(type: String): List<String> {
if (!config.contains("patterns.$type.args")) throw IllegalArgumentException("服务端样板 $type 的配置不存在,请检查settings.yml")
return config.getStringList("patterns.$type.args")
}
}
}
@@ -0,0 +1,29 @@
package cc.maxmc.msm.controlled.settings
import cc.maxmc.msm.controlled.MultiServerMan
import net.md_5.bungee.api.ProxyServer
import net.md_5.bungee.api.chat.TextComponent
import net.md_5.bungee.config.Configuration
import net.md_5.bungee.config.ConfigurationProvider
import net.md_5.bungee.config.YamlConfiguration
import kotlin.io.path.*
object SettingsReader {
private val file = MultiServerMan.instance.dataFolder.toPath().resolve("settings.yml")
val config: Configuration
private val basePatternFolder = MultiServerMan.instance.dataFolder.toPath().resolve("patterns")
init {
if (!basePatternFolder.exists()) {
basePatternFolder.createDirectories()
}
if (!file.exists()) {
MultiServerMan.instance.dataFolder.toPath().createDirectories()
val stream = MultiServerMan.instance.getResourceAsStream("settings.yml")
file.createFile()
stream.copyTo(file.outputStream())
ProxyServer.getInstance().console.sendMessage(TextComponent("§b| §7配置文件不存在,正在创建配置文件。"))
}
config = ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(file.inputStream())
}
}
@@ -0,0 +1,55 @@
package cc.maxmc.msm.controlled.utils
import cc.maxmc.msm.common.utils.log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.nio.file.Path
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.concurrent.thread
import kotlin.io.path.extension
import kotlin.io.path.name
class ScriptRunner(private val scriptPath: Path) {
private lateinit var process: Process
lateinit var thread: Thread
private val listeners = CopyOnWriteArrayList<Pair<String, () -> Unit>>()
private val writer by lazy { process.outputStream.bufferedWriter() }
private val reader by lazy { process.inputStream.bufferedReader() }
fun launch(vararg args: String) {
val builder = ProcessBuilder().directory(scriptPath.parent.toFile())
if (scriptPath.extension == "sh") {
builder.command("bash", scriptPath.name, *args)
} else {
builder.command("powershell.exe", scriptPath.name, *args)
}
process = builder.start()
log("§c§lProcess Started")
thread = thread {
while (true) {
if (listeners.isEmpty()) {
continue
}
var line = ""
while (reader.readLine()?.also { line = it } != null) {
listeners.forEach {
if (line.contains(it.first)) it.second()
}
}
}
}
}
fun exec(command: String) {
writer.appendLine(command)
writer.flush()
}
fun onOutput(string: String, func: () -> Unit) {
listeners.add(string to func)
}
suspend fun exit(): Int = withContext(Dispatchers.IO) {
process.waitFor()
}
}
+3
View File
@@ -0,0 +1,3 @@
name: Controlled
main: cc.maxmc.msm.controlled.MultiServerMan
author: MistyRain
@@ -0,0 +1,14 @@
# 主BC的地址及端口
parent:
address: 127.0.0.1
port: 23333
# 该子BC所在机器所能开放的端口
ports:
- 30000
- 30001..30019
# 该子BC所支持的服务端模版类型及模版类型启动所需参数
patterns:
main:
args: [ "-ListenPort=${port}", "-usr=usr", "-pwd=pwd" ]