From a6f0150a404acc487681e119d8a4dbdaaf19b46a Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 00:27:34 +0200 Subject: [PATCH 01/25] feat: implement core launcher with Redis integration and server state management --- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 45457 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 7 +- gradlew | 2 +- gradlew.bat | 31 ++--- settings.gradle.kts | 4 + .../server/state/ExternalSurfServerState.kt | 10 ++ .../surf-core-launcher-api/build.gradle.kts | 11 ++ .../core/launcher/api/LauncherConstants.kt | 5 + .../build.gradle.kts | 18 +++ .../surf/core/launcher/server/CoreLauncher.kt | 111 ++++++++++++++++++ .../server/CoreLauncherEnvironment.kt | 13 ++ .../server/config/CoreLauncherConfig.kt | 22 ++++ .../server/ping/MinecraftServerPinger.kt | 39 ++++++ 14 files changed, 250 insertions(+), 25 deletions(-) create mode 100644 surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt create mode 100644 surf-core-launcher/surf-core-launcher-api/build.gradle.kts create mode 100644 surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/build.gradle.kts create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt diff --git a/gradle.properties b/gradle.properties index 94db2d5c..32c46119 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official kotlin.stdlib.default.dependency=false org.gradle.parallel=true -version=2.3.0-SNAPSHOT \ No newline at end of file +version=2.4.0-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c75ab801e22807dde59e12a8735a34077..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39796 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ɵ=r6CZm*ctobRli@Q9X4qo$){P6@xZ0Eq-I) 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%0Aonjyf`q#Bg+;H(_UceX22V^&|e4K_eG#r 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 I`1hxO0p@%Q`v3p{ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ec2e3c24..b52fb7e7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,9 @@ -#Sun Apr 05 17:57:20 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index adff685a..b9bb139f 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3ab..24c62d56 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index c6904527..3f56fae5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,7 @@ include("surf-core-velocity") // Microservice include("surf-core-microservice") + +include("surf-core-launcher") +include("surf-core-launcher:surf-core-launcher-api") +include("surf-core-launcher:surf-core-launcher-server") \ No newline at end of file diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt new file mode 100644 index 00000000..07bc3387 --- /dev/null +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.core.api.common.server.state + +enum class ExternalSurfServerState { + STARTING, + ONLINE, + STOPPING, + OFFLINE, + UNREACHABLE, + CRASHED +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-api/build.gradle.kts b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts new file mode 100644 index 00000000..0d542d6c --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("dev.slne.surf.api.gradle.core") +} + +surfCoreApi { + withSurfRedis() +} + +dependencies { + api(projects.surfCoreApi.surfCoreApiCommon) +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt new file mode 100644 index 00000000..09982dda --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt @@ -0,0 +1,5 @@ +package dev.slne.surf.core.launcher.api + +object LauncherConstants { + const val PROPERTY_LAUNCHED_BY_CORE = "LAUNCHED_BY_CORE" +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts new file mode 100644 index 00000000..6509e3ae --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("dev.slne.surf.api.gradle.standalone") +} + +dependencies { + api(projects.surfCoreLauncher.surfCoreLauncherApi) + implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") +} + +tasks.shadowJar { + relocate("dev.slne.surf.redis", "dev.slne.surf.core.launcher.libs") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "dev.slne.surf.core.launcher.server.CoreLauncherKt" + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt new file mode 100644 index 00000000..e8cce9a4 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -0,0 +1,111 @@ +package dev.slne.surf.core.launcher.server + +import dev.slne.surf.api.core.util.logger +import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap +import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState +import dev.slne.surf.core.launcher.api.LauncherConstants +import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig +import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger +import dev.slne.surf.redis.RedisApi +import kotlinx.coroutines.* +import java.util.concurrent.TimeUnit + +val logger = logger() + +object CoreLauncher { + val redisApi = RedisApi.create("surf-core-launcher") + private val redisStatusMap = + redisApi.createSyncMap("server_status") + + fun getCurrentServerState(): ExternalSurfServerState? = + redisStatusMap[CoreLauncherConfig.getConfig().serverName] + + fun updateServerState(newState: ExternalSurfServerState) { + redisStatusMap[CoreLauncherConfig.getConfig().serverName] = newState + } + + private fun buildStartupCommand(): List { + val base = CoreLauncherConfig.getConfig().serverStartupCommand + val parts = base.split(" ").toMutableList() + + if (parts.none { it == "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true" }) { + parts.add(1, "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true") + } + + if (parts.none { it.startsWith("-Xms") }) { + parts.add(1, "-Xms${CoreLauncherEnvironment.MC_MEMORY_MIN}M") + } + + if (parts.none { it.startsWith("-Xmx") }) { + parts.add(1, "-Xmx${CoreLauncherEnvironment.MC_MEMORY_MAX}M") + } + + return parts + } + + + val serverProcess: Process = ProcessBuilder( + buildStartupCommand() + ) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .start() + + + suspend fun launch(args: Array) { + SurfApiStandaloneBootstrap.bootstrap() + SurfApiStandaloneBootstrap.enable() + + withContext(Dispatchers.IO) { + redisApi.freezeAndConnect() + logger.atInfo().log("[CoreLauncher] Connected to Redis!") + + updateServerState(ExternalSurfServerState.STARTING) + } + + MinecraftServerPinger.build() + + logger.atInfo().log("[CoreLauncher] Server process started! (PID: ${serverProcess.pid()})") + + coroutineScope { + launch { + awaitCancellation() + }.join() + } + } + + + fun shutdown() { + logger.atInfo().log("[CoreLauncher] Shutdown signal received, stopping server...") + updateServerState(ExternalSurfServerState.STOPPING) + + serverProcess.destroy() + + if (serverProcess.isAlive) { + serverProcess.waitFor(30, TimeUnit.SECONDS) + } + + updateServerState(ExternalSurfServerState.OFFLINE) + redisApi.disconnect() + + if (serverProcess.isAlive) { + logger.atWarning() + .log("[CoreLauncher] Server process did not stop gracefully within 30s, waiting longer... (no force yet)") + serverProcess.destroy() + } else { + logger.atInfo().log("[CoreLauncher] Server process stopped gracefully.") + } + } +} + +suspend fun main(args: Array) { + Runtime.getRuntime().addShutdownHook(Thread { + runBlocking { + CoreLauncher.shutdown() + SurfApiStandaloneBootstrap.shutdown() + } + }) + + CoreLauncher.launch(args) +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt new file mode 100644 index 00000000..ab1fe6a3 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.core.launcher.server + +object CoreLauncherEnvironment { + val MC_MEMORY_MAX = + System.getenv("SERVER_MEMORY") ?: error("SERVER_MEMORY environment variable is not set") + val MC_MEMORY_MIN = System.getenv("SERVER_MEMORY_MIN") + ?: error("SERVER_MEMORY_MIN environment variable is not set") + val SERVER_PORT = System.getenv("SERVER_PORT")?.toInt() + ?: error("SERVER_PORT environment variable is not set or is not a valid integer") + + val STARTUP_COMMAND = System.getenv("STARTUP") + ?: error("STARTUP environment variable is not set") +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt new file mode 100644 index 00000000..550bb85b --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt @@ -0,0 +1,22 @@ +package dev.slne.surf.core.launcher.server.config + +import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.serializer.java.uuid.SerializableUUID +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.util.* +import kotlin.io.path.Path + +@ConfigSerializable +data class CoreLauncherConfig( + val serverStartupCommand: String = "java -jar server.jar --nogui", + val serverName: String = "unknown", + val serverDisplayName: String = "unknown", + val serverCategory: String = "unknown", + val serverUuid: SerializableUUID = UUID.randomUUID() +) { + companion object : SpongeYmlConfigClass( + CoreLauncherConfig::class.java, + Path("."), + "core-launcher-config.yml" + ) +} diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt new file mode 100644 index 00000000..f1fcf4be --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -0,0 +1,39 @@ +package dev.slne.surf.core.launcher.server.ping + +import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState +import dev.slne.surf.core.launcher.server.CoreLauncher +import dev.slne.surf.core.launcher.server.CoreLauncherEnvironment +import dev.slne.surf.core.launcher.server.logger +import kotlinx.coroutines.delay +import java.io.IOException +import java.net.Socket +import kotlin.time.Duration.Companion.milliseconds + +object MinecraftServerPinger { + suspend fun build() { + val process = CoreLauncher.serverProcess + + while (process.isAlive) { + delay(5_000L.milliseconds) + + val reachable = pingServer(CoreLauncherEnvironment.SERVER_PORT) + val currentStatus = CoreLauncher.getCurrentServerState() + + if (reachable) { + if (currentStatus != ExternalSurfServerState.ONLINE) { + logger.atInfo().log("[Launcher] Server is ONLINE") + } + } else { + if (currentStatus == ExternalSurfServerState.ONLINE) { + logger.atWarning().log("[Launcher] Server UNREACHABLE") + } + } + } + } + + private fun pingServer(port: Int): Boolean = try { + Socket("127.0.0.1", port).use { true } + } catch (e: IOException) { + false + } +} \ No newline at end of file From 1e8e3ef4812dec913d3c567a05c888ef3de03fd1 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 11:14:35 +0200 Subject: [PATCH 02/25] refactor: remove Redis integration and simplify server state management --- .../build.gradle.kts | 2 +- .../surf/core/launcher/server/CoreLauncher.kt | 32 +++++++++---------- .../server/CoreLauncherEnvironment.kt | 5 --- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 6509e3ae..662d6cd0 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) - implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") + compileOnly("dev.slne.surf.redis:surf-redis-api:1.6.0") } tasks.shadowJar { diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index e8cce9a4..9ff51391 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -6,22 +6,28 @@ import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger -import dev.slne.surf.redis.RedisApi import kotlinx.coroutines.* import java.util.concurrent.TimeUnit val logger = logger() object CoreLauncher { - val redisApi = RedisApi.create("surf-core-launcher") - private val redisStatusMap = - redisApi.createSyncMap("server_status") +// val redisApi = RedisApi.create("surf-core-launcher") +// private val redisStatusMap = +// redisApi.createSyncMap("server_status") - fun getCurrentServerState(): ExternalSurfServerState? = - redisStatusMap[CoreLauncherConfig.getConfig().serverName] + + var currentState: ExternalSurfServerState = ExternalSurfServerState.OFFLINE + set(value) { + field = value + logger.atInfo().log("[CoreLauncher] Server state changed to: $value") + } + + fun getCurrentServerState(): ExternalSurfServerState = + currentState fun updateServerState(newState: ExternalSurfServerState) { - redisStatusMap[CoreLauncherConfig.getConfig().serverName] = newState + currentState = newState } private fun buildStartupCommand(): List { @@ -32,14 +38,6 @@ object CoreLauncher { parts.add(1, "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true") } - if (parts.none { it.startsWith("-Xms") }) { - parts.add(1, "-Xms${CoreLauncherEnvironment.MC_MEMORY_MIN}M") - } - - if (parts.none { it.startsWith("-Xmx") }) { - parts.add(1, "-Xmx${CoreLauncherEnvironment.MC_MEMORY_MAX}M") - } - return parts } @@ -58,7 +56,7 @@ object CoreLauncher { SurfApiStandaloneBootstrap.enable() withContext(Dispatchers.IO) { - redisApi.freezeAndConnect() +// redisApi.freezeAndConnect() logger.atInfo().log("[CoreLauncher] Connected to Redis!") updateServerState(ExternalSurfServerState.STARTING) @@ -87,7 +85,7 @@ object CoreLauncher { } updateServerState(ExternalSurfServerState.OFFLINE) - redisApi.disconnect() +// redisApi.disconnect() if (serverProcess.isAlive) { logger.atWarning() diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt index ab1fe6a3..4bbea402 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt @@ -3,11 +3,6 @@ package dev.slne.surf.core.launcher.server object CoreLauncherEnvironment { val MC_MEMORY_MAX = System.getenv("SERVER_MEMORY") ?: error("SERVER_MEMORY environment variable is not set") - val MC_MEMORY_MIN = System.getenv("SERVER_MEMORY_MIN") - ?: error("SERVER_MEMORY_MIN environment variable is not set") val SERVER_PORT = System.getenv("SERVER_PORT")?.toInt() ?: error("SERVER_PORT environment variable is not set or is not a valid integer") - - val STARTUP_COMMAND = System.getenv("STARTUP") - ?: error("STARTUP environment variable is not set") } \ No newline at end of file From 5b62e34a8ab40464007b415b66ef3a6798c44df8 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 11:14:48 +0200 Subject: [PATCH 03/25] refactor: remove Redis integration and simplify server state management --- surf-core-launcher/surf-core-launcher-server/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 662d6cd0..6509e3ae 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -4,7 +4,7 @@ plugins { dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) - compileOnly("dev.slne.surf.redis:surf-redis-api:1.6.0") + implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") } tasks.shadowJar { From d2e1235e5f8d3b5b142d026f4393febb5d7e8c74 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 12:24:09 +0200 Subject: [PATCH 04/25] refactor: replace logger with console output and improve server state handling --- .../surf/core/launcher/server/CoreLauncher.kt | 45 ++++++++---------- .../server/ping/MinecraftServerPinger.kt | 46 ++++++++++++++----- .../src/main/resources/logging.properties | 5 ++ 3 files changed, 58 insertions(+), 38 deletions(-) create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 9ff51391..11bedebe 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -1,15 +1,16 @@ package dev.slne.surf.core.launcher.server -import dev.slne.surf.api.core.util.logger import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit -val logger = logger() +const val LOG_PREFIX = "\u001B[0;91m[CoreLauncher]\u001B[0m" object CoreLauncher { // val redisApi = RedisApi.create("surf-core-launcher") @@ -20,7 +21,7 @@ object CoreLauncher { var currentState: ExternalSurfServerState = ExternalSurfServerState.OFFLINE set(value) { field = value - logger.atInfo().log("[CoreLauncher] Server state changed to: $value") + println("$LOG_PREFIX Server state changed to: $value") } fun getCurrentServerState(): ExternalSurfServerState = @@ -41,14 +42,7 @@ object CoreLauncher { return parts } - - val serverProcess: Process = ProcessBuilder( - buildStartupCommand() - ) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .redirectInput(ProcessBuilder.Redirect.INHERIT) - .start() + lateinit var serverProcess: Process suspend fun launch(args: Array) { @@ -56,26 +50,26 @@ object CoreLauncher { SurfApiStandaloneBootstrap.enable() withContext(Dispatchers.IO) { -// redisApi.freezeAndConnect() - logger.atInfo().log("[CoreLauncher] Connected to Redis!") - + println("$LOG_PREFIX Connected to Redis!") updateServerState(ExternalSurfServerState.STARTING) - } - MinecraftServerPinger.build() + serverProcess = ProcessBuilder(buildStartupCommand()) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .start() - logger.atInfo().log("[CoreLauncher] Server process started! (PID: ${serverProcess.pid()})") + println("$LOG_PREFIX Server process started! (PID: ${serverProcess.pid()})") - coroutineScope { - launch { - awaitCancellation() - }.join() + runCatching { + MinecraftServerPinger.build() + } } } fun shutdown() { - logger.atInfo().log("[CoreLauncher] Shutdown signal received, stopping server...") + println("$LOG_PREFIX Shutdown signal received, stopping server...") updateServerState(ExternalSurfServerState.STOPPING) serverProcess.destroy() @@ -88,11 +82,10 @@ object CoreLauncher { // redisApi.disconnect() if (serverProcess.isAlive) { - logger.atWarning() - .log("[CoreLauncher] Server process did not stop gracefully within 30s, waiting longer... (no force yet)") + println("$LOG_PREFIX Warning: Server process did not stop gracefully within 30s, waiting longer... (no force yet)") serverProcess.destroy() } else { - logger.atInfo().log("[CoreLauncher] Server process stopped gracefully.") + println("$LOG_PREFIX Server process stopped gracefully.") } } } diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt index f1fcf4be..df2b30f0 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -3,8 +3,8 @@ package dev.slne.surf.core.launcher.server.ping import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.CoreLauncherEnvironment -import dev.slne.surf.core.launcher.server.logger -import kotlinx.coroutines.delay +import dev.slne.surf.core.launcher.server.LOG_PREFIX +import kotlinx.coroutines.* import java.io.IOException import java.net.Socket import kotlin.time.Duration.Companion.milliseconds @@ -13,21 +13,43 @@ object MinecraftServerPinger { suspend fun build() { val process = CoreLauncher.serverProcess - while (process.isAlive) { - delay(5_000L.milliseconds) + coroutineScope { + launch { + process.waitFor() - val reachable = pingServer(CoreLauncherEnvironment.SERVER_PORT) - val currentStatus = CoreLauncher.getCurrentServerState() + delay(500.milliseconds) - if (reachable) { - if (currentStatus != ExternalSurfServerState.ONLINE) { - logger.atInfo().log("[Launcher] Server is ONLINE") + val lastStatus = CoreLauncher.getCurrentServerState() + if (lastStatus != ExternalSurfServerState.OFFLINE && lastStatus != ExternalSurfServerState.STOPPING) { + println("$LOG_PREFIX Server process died unexpectedly → CRASHED") + CoreLauncher.updateServerState(ExternalSurfServerState.CRASHED) } - } else { - if (currentStatus == ExternalSurfServerState.ONLINE) { - logger.atWarning().log("[Launcher] Server UNREACHABLE") + + this@coroutineScope.cancel() + } + + launch { + while (process.isAlive) { + delay(5_000L.milliseconds) + + val reachable = pingServer(CoreLauncherEnvironment.SERVER_PORT) + val currentStatus = CoreLauncher.getCurrentServerState() + + if (reachable) { + if (currentStatus != ExternalSurfServerState.ONLINE) { + println("$LOG_PREFIX Server is ONLINE") + CoreLauncher.updateServerState(ExternalSurfServerState.ONLINE) + } + } else { + if (currentStatus == ExternalSurfServerState.ONLINE) { + println("$LOG_PREFIX Server UNREACHABLE") + CoreLauncher.updateServerState(ExternalSurfServerState.UNREACHABLE) + } + } } } + + awaitCancellation() } } diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties b/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties new file mode 100644 index 00000000..f9533261 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=\u001B[0;91m[CoreLauncher]\u001B[0m %4$s: %5$s%n \ No newline at end of file From db5bba6655bda5b71670a566a45f52f591a398f3 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 13:47:20 +0200 Subject: [PATCH 05/25] refactor: enhance server launch process and state management --- ...urfServerState.kt => SurfServiceStatus.kt} | 6 +- .../api/redis/ServiceStatusRedisEvent.kt | 10 ++ .../surf/core/launcher/server/CoreLauncher.kt | 120 ++++++++++-------- .../server/config/CoreLauncherConfig.kt | 1 + .../server/ping/MinecraftServerPinger.kt | 88 +++++++------ .../core/launcher/server/ping/ping-util.kt | 45 +++++++ 6 files changed, 169 insertions(+), 101 deletions(-) rename surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/{ExternalSurfServerState.kt => SurfServiceStatus.kt} (54%) create mode 100644 surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt similarity index 54% rename from surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt rename to surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt index 07bc3387..05889e23 100644 --- a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/ExternalSurfServerState.kt +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt @@ -1,10 +1,8 @@ package dev.slne.surf.core.api.common.server.state -enum class ExternalSurfServerState { - STARTING, +enum class SurfServiceStatus { + LAUNCHING, ONLINE, - STOPPING, - OFFLINE, UNREACHABLE, CRASHED } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt new file mode 100644 index 00000000..1f480841 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.core.launcher.api.redis + +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus +import kotlinx.serialization.Serializable + +@Serializable +data class ServiceStatusRedisEvent( + val serviceName: String, + val status: SurfServiceStatus +) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 11bedebe..9ccff909 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -1,92 +1,98 @@ package dev.slne.surf.core.launcher.server import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap -import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean const val LOG_PREFIX = "\u001B[0;91m[CoreLauncher]\u001B[0m" object CoreLauncher { -// val redisApi = RedisApi.create("surf-core-launcher") -// private val redisStatusMap = -// redisApi.createSyncMap("server_status") + private val shuttingDown = AtomicBoolean(false) + lateinit var serverProcess: Process + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var monitorJob: Job? = null + var minecraftServerOnline: Boolean = false + suspend fun launch() { + SurfApiStandaloneBootstrap.bootstrap() + SurfApiStandaloneBootstrap.enable() - var currentState: ExternalSurfServerState = ExternalSurfServerState.OFFLINE - set(value) { - field = value - println("$LOG_PREFIX Server state changed to: $value") - } + println("$LOG_PREFIX Starting Minecraft Server...") - fun getCurrentServerState(): ExternalSurfServerState = - currentState + val command = buildStartupCommand() - fun updateServerState(newState: ExternalSurfServerState) { - currentState = newState - } + serverProcess = ProcessBuilder(command) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() - private fun buildStartupCommand(): List { - val base = CoreLauncherConfig.getConfig().serverStartupCommand - val parts = base.split(" ").toMutableList() + println("$LOG_PREFIX Server process started") - if (parts.none { it == "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true" }) { - parts.add(1, "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true") - } + monitorJob = scope.launch { - return parts - } + launch { + serverProcess.inputStream.bufferedReader().forEachLine { line -> + println(line) - lateinit var serverProcess: Process + if (line.contains( + CoreLauncherConfig.getConfig().startedMessage, + ignoreCase = true + ) + ) { + minecraftServerOnline = true + println("$LOG_PREFIX Server is now online.") + } + } + } + launch { + serverProcess.errorStream.bufferedReader().forEachLine { line -> + println(line) + } + } - suspend fun launch(args: Array) { - SurfApiStandaloneBootstrap.bootstrap() - SurfApiStandaloneBootstrap.enable() + MinecraftServerPinger.monitor(serverProcess) + } + } - withContext(Dispatchers.IO) { - println("$LOG_PREFIX Connected to Redis!") - updateServerState(ExternalSurfServerState.STARTING) + suspend fun shutdown() { + println("$LOG_PREFIX Shutting down launcher/server...") - serverProcess = ProcessBuilder(buildStartupCommand()) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .redirectInput(ProcessBuilder.Redirect.INHERIT) - .start() + monitorJob?.cancelAndJoin() - println("$LOG_PREFIX Server process started! (PID: ${serverProcess.pid()})") + if (::serverProcess.isInitialized && serverProcess.isAlive) { + serverProcess.destroy() - runCatching { - MinecraftServerPinger.build() + if (!serverProcess.waitFor(45, TimeUnit.SECONDS)) { + println("$LOG_PREFIX Server did not stop within 45 seconds") } } - } + scope.cancel() + } - fun shutdown() { - println("$LOG_PREFIX Shutdown signal received, stopping server...") - updateServerState(ExternalSurfServerState.STOPPING) + fun isShuttingDown(): Boolean = shuttingDown.get() - serverProcess.destroy() + private fun buildStartupCommand(): List { + val base = CoreLauncherConfig.getConfig().serverStartupCommand - if (serverProcess.isAlive) { - serverProcess.waitFor(30, TimeUnit.SECONDS) - } + val parts = Regex("""[^\s"]+|"([^"]*)"""") + .findAll(base) + .map { it.value.replace("\"", "") } + .toMutableList() - updateServerState(ExternalSurfServerState.OFFLINE) -// redisApi.disconnect() + val flag = "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true" - if (serverProcess.isAlive) { - println("$LOG_PREFIX Warning: Server process did not stop gracefully within 30s, waiting longer... (no force yet)") - serverProcess.destroy() - } else { - println("$LOG_PREFIX Server process stopped gracefully.") + if (parts.none { it == flag }) { + parts.add(1, flag) } + + return parts } } @@ -98,5 +104,7 @@ suspend fun main(args: Array) { } }) - CoreLauncher.launch(args) + CoreLauncher.launch() + CoreLauncher.serverProcess.waitFor() + SurfApiStandaloneBootstrap.shutdown() } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt index 550bb85b..37310b6b 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt @@ -9,6 +9,7 @@ import kotlin.io.path.Path @ConfigSerializable data class CoreLauncherConfig( val serverStartupCommand: String = "java -jar server.jar --nogui", + val startedMessage: String = "For help, type \"help\"", val serverName: String = "unknown", val serverDisplayName: String = "unknown", val serverCategory: String = "unknown", diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt index df2b30f0..0dd37b6e 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -1,61 +1,67 @@ package dev.slne.surf.core.launcher.server.ping -import dev.slne.surf.core.api.common.server.state.ExternalSurfServerState import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.CoreLauncherEnvironment import dev.slne.surf.core.launcher.server.LOG_PREFIX import kotlinx.coroutines.* -import java.io.IOException +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.InetSocketAddress import java.net.Socket -import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds object MinecraftServerPinger { - suspend fun build() { - val process = CoreLauncher.serverProcess + suspend fun monitor(process: Process): Unit = coroutineScope { + launch { + val exitCode = process.waitFor() + if (CoreLauncher.isShuttingDown()) { + return@launch + } - coroutineScope { - launch { - process.waitFor() + if (exitCode == 0) { + println("$LOG_PREFIX Minecraft server stopped") + } else { + println("$LOG_PREFIX Minecraft server crashed!") + } + } - delay(500.milliseconds) + launch { + while (process.isAlive && CoreLauncher.minecraftServerOnline) { + delay(5.seconds) - val lastStatus = CoreLauncher.getCurrentServerState() - if (lastStatus != ExternalSurfServerState.OFFLINE && lastStatus != ExternalSurfServerState.STOPPING) { - println("$LOG_PREFIX Server process died unexpectedly → CRASHED") - CoreLauncher.updateServerState(ExternalSurfServerState.CRASHED) + val reachable = withContext(Dispatchers.IO) { + isMinecraftReady(CoreLauncherEnvironment.SERVER_PORT) } - this@coroutineScope.cancel() - } - - launch { - while (process.isAlive) { - delay(5_000L.milliseconds) - - val reachable = pingServer(CoreLauncherEnvironment.SERVER_PORT) - val currentStatus = CoreLauncher.getCurrentServerState() - - if (reachable) { - if (currentStatus != ExternalSurfServerState.ONLINE) { - println("$LOG_PREFIX Server is ONLINE") - CoreLauncher.updateServerState(ExternalSurfServerState.ONLINE) - } - } else { - if (currentStatus == ExternalSurfServerState.ONLINE) { - println("$LOG_PREFIX Server UNREACHABLE") - CoreLauncher.updateServerState(ExternalSurfServerState.UNREACHABLE) - } - } + if (!reachable) { + println("$LOG_PREFIX Minecraft server is unreachable") } } - - awaitCancellation() } - } - private fun pingServer(port: Int): Boolean = try { - Socket("127.0.0.1", port).use { true } - } catch (e: IOException) { - false + awaitCancellation() } + + private fun isMinecraftReady(port: Int): Boolean = + runCatching { + Socket().use { socket -> + val host = "127.0.0.1" + socket.connect(InetSocketAddress(host, port), 1500) + socket.soTimeout = 1500 + + DataOutputStream(socket.getOutputStream()).use { output -> + DataInputStream(socket.getInputStream()).use { input -> + val handshake = buildHandshakePacket(host, port) + + output.write(handshake) + output.write(byteArrayOf(0x01, 0x00)) + output.flush() + + input.readByte() + + true + } + } + } + }.getOrDefault(false) } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt new file mode 100644 index 00000000..214e60b4 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.core.launcher.server.ping + +@Suppress("SameParameterValue") +fun buildHandshakePacket( + host: String, + port: Int +): ByteArray { + val hostBytes = host.toByteArray() + + val data = mutableListOf() + + data += 0x00 + data += writeVarInt(763).toList() + + data += writeVarInt(hostBytes.size).toList() + data += hostBytes.toList() + + data += ((port shr 8) and 0xFF).toByte() + data += (port and 0xFF).toByte() + + data += 0x01 + + val packetLength = writeVarInt(data.size) + + return packetLength + data.toByteArray() +} + +fun writeVarInt(value: Int): ByteArray { + var current = value + val output = mutableListOf() + + do { + var temp = (current and 0b01111111) + + current = current ushr 7 + + if (current != 0) { + temp = temp or 0b10000000 + } + + output += temp.toByte() + } while (current != 0) + + return output.toByteArray() +} \ No newline at end of file From 16df0b5b2ef8e4d1a22396cd54e517463cfd9c8b Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 15:50:41 +0200 Subject: [PATCH 06/25] feat: implement plugin updater with GitHub integration and auto-update feature --- .../build.gradle.kts | 1 + .../surf/core/launcher/server/CoreLauncher.kt | 16 +++ .../server/config/CoreLauncherConfig.kt | 4 +- .../server/updater/UpdatablePlugin.kt | 30 +++++ .../updater/cooldown/UpdateCooldownTracker.kt | 52 ++++++++ .../server/updater/github/GitHubClient.kt | 50 ++++++++ .../server/updater/process/PluginScanner.kt | 56 +++++++++ .../server/updater/process/PluginUpdater.kt | 117 ++++++++++++++++++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt create mode 100644 surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 6509e3ae..4ccecb06 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") } tasks.shadowJar { diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 9ccff909..ac76b3e1 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -4,9 +4,11 @@ import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger +import dev.slne.surf.core.launcher.server.updater.process.PluginUpdater import kotlinx.coroutines.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.time.Duration.Companion.seconds const val LOG_PREFIX = "\u001B[0;91m[CoreLauncher]\u001B[0m" @@ -21,6 +23,20 @@ object CoreLauncher { SurfApiStandaloneBootstrap.bootstrap() SurfApiStandaloneBootstrap.enable() + if (CoreLauncherConfig.getConfig().autoUpdateSurfPlugins) { + println("$LOG_PREFIX Searching plugin updates...") + + withTimeoutOrNull(20.seconds) { + if (CoreLauncherConfig.getConfig().personalAccessToken.isBlank()) { + println("$LOG_PREFIX No GitHub personal access token provided, skipping plugin update check") + return@withTimeoutOrNull + } + + PluginUpdater.start() + } + ?: println("$LOG_PREFIX Plugin update check timed out after 30 seconds, continuing with server startup") + } + println("$LOG_PREFIX Starting Minecraft Server...") val command = buildStartupCommand() diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt index 37310b6b..5f0a820f 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt @@ -13,7 +13,9 @@ data class CoreLauncherConfig( val serverName: String = "unknown", val serverDisplayName: String = "unknown", val serverCategory: String = "unknown", - val serverUuid: SerializableUUID = UUID.randomUUID() + val serverUuid: SerializableUUID = UUID.randomUUID(), + val autoUpdateSurfPlugins: Boolean = true, + val personalAccessToken: String = "" ) { companion object : SpongeYmlConfigClass( CoreLauncherConfig::class.java, diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt new file mode 100644 index 00000000..657fec26 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt @@ -0,0 +1,30 @@ +package dev.slne.surf.core.launcher.server.updater + +import java.nio.file.Path + + +/** + * Represents a plugin that can be updated. + * + * @property name The name of the plugin, e.g. surf-core-paper, surf-api-paper-server, surf-captcha or surf-core-velocity. + * @property currentVersion The current version of the plugin. + */ +data class UpdatablePlugin( + val name: String, + val currentVersion: String, + val jarPath: Path +) { + /** + * The Plugin name, e.g. core, captcha or api + */ + fun findPluginName() = + name.substringAfter("surf-").substringBefore("-paper").substringBefore("-velocity") + + fun findPluginType() = when { + name.contains("-paper") -> "paper" + name.contains("-velocity") -> "velocity" + else -> "velocity" + } + + val latestReleaseUrl get() = "https://api.github.com/repos/SLNE-DEVELOPMENT/surf-${findPluginName()}/releases/latest" +} diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt new file mode 100644 index 00000000..906d4897 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt @@ -0,0 +1,52 @@ +package dev.slne.surf.core.launcher.server.updater.cooldown + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +class UpdateCooldownTracker(private val persistencePath: Path) { + private val lastUpdated = ConcurrentHashMap() + private val cooldownMs = TimeUnit.MINUTES.toMillis(10) + + init { + load() + } + + fun isOnCooldown(pluginName: String): Boolean { + val lastUpdate = lastUpdated[pluginName] ?: return false + return System.currentTimeMillis() - lastUpdate < cooldownMs + } + + fun remainingSeconds(pluginName: String): Long { + val lastUpdate = lastUpdated[pluginName] ?: return 0 + return (cooldownMs - (System.currentTimeMillis() - lastUpdate)) / 1000 + } + + fun markUpdated(pluginName: String) { + lastUpdated[pluginName] = System.currentTimeMillis() + save() + } + + private fun load() { + if (!Files.exists(persistencePath)) { + return + } + + Files.readAllLines(persistencePath).forEach { line -> + val parts = line.split("=", limit = 2) + if (parts.size != 2) return@forEach + lastUpdated[parts[0].trim()] = parts[1].trim().toLongOrNull() ?: return@forEach + } + } + + private fun save() { + Files.writeString( + persistencePath, + lastUpdated.entries.joinToString("\n") { (name, ts) -> "$name=$ts" }, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ) + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt new file mode 100644 index 00000000..4679bb9a --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt @@ -0,0 +1,50 @@ +package dev.slne.surf.core.launcher.server.updater.github + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import java.net.HttpURLConnection +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption + +class GitHubClient(private val token: String?) { + + private val mapper = jacksonObjectMapper() + + fun fetchLatestRelease(plugin: UpdatablePlugin): Map? { + return try { + val connection = openConnection(plugin.latestReleaseUrl, "application/vnd.github+json") + connection.setRequestProperty("X-GitHub-Api-Version", "2022-11-28") + connection.connect() + + if (connection.responseCode != 200) { + println("[GitHub] API antwortete mit ${connection.responseCode} für ${plugin.name}") + return null + } + + connection.inputStream.use { input -> + mapper.readValue(input, object : TypeReference>() {}) + } + } catch (e: Exception) { + null + } + } + + fun downloadAsset(url: String, target: Path) { + val connection = openConnection(url, "application/octet-stream") + connection.connect() + + connection.inputStream.use { input -> + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) + } + } + + private fun openConnection(url: String, accept: String): HttpURLConnection { + val connection = URI(url).toURL().openConnection() as HttpURLConnection + connection.setRequestProperty("Accept", accept) + token?.let { connection.setRequestProperty("Authorization", "Bearer $it") } + return connection + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt new file mode 100644 index 00000000..0cd8318e --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt @@ -0,0 +1,56 @@ +package dev.slne.surf.core.launcher.server.updater.process + +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.yaml.snakeyaml.Yaml +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.extension +import kotlin.io.path.name + +class PluginScanner(private val pluginsPath: Path) { + private val yaml = Yaml() + + suspend fun findPlugins(): List = withContext(Dispatchers.IO) { + if (!Files.exists(pluginsPath)) { + return@withContext emptyList() + } + + Files.list(pluginsPath).use { stream -> + stream + .filter { it.extension == "jar" } + .filter { it.name.startsWith("surf-") } + .toList() + .mapNotNull { readPlugin(it) } + .toList() + } + } + + private fun readPlugin(jarPath: Path): UpdatablePlugin? = runCatching { + JarFile(jarPath.toFile()).use { jar -> + val entry = jar.entries().asSequence().firstOrNull { + it.name == "velocity-plugin.yml" || + it.name == "paper-plugin.yml" || + it.name == "plugin.yml" + } ?: return null + + jar.getInputStream(entry).use { input -> + val data = yaml.load>(input) ?: return null + val version = data["version"]?.toString() ?: return null + + val name = when (entry.name) { + "velocity-plugin.yml" -> data["id"]?.toString() + else -> data["name"]?.toString() + } ?: return null + + UpdatablePlugin( + name = name, + currentVersion = version, + jarPath = jarPath + ) + } + } + }.getOrNull() +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt new file mode 100644 index 00000000..ac8ad6f1 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -0,0 +1,117 @@ +package dev.slne.surf.core.launcher.server.updater.process + +import dev.slne.surf.core.launcher.server.LOG_PREFIX +import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import dev.slne.surf.core.launcher.server.updater.cooldown.UpdateCooldownTracker +import dev.slne.surf.core.launcher.server.updater.github.GitHubClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.system.measureTimeMillis + +object PluginUpdater { + private val pluginsPath = Path.of("plugins") + private val oldPath = pluginsPath.resolve("old") + + private val scanner = PluginScanner(pluginsPath) + private val gitHubClient = GitHubClient(CoreLauncherConfig.getConfig().personalAccessToken) + private val cooldownTracker = UpdateCooldownTracker(pluginsPath.resolve(".last-updates")) + + suspend fun start() { + val plugins = scanner.findPlugins() + + if (plugins.isEmpty()) { + return + } + + withContext(Dispatchers.IO) { + if (!Files.exists(oldPath)) Files.createDirectories(oldPath) + } + + println("$LOG_PREFIX (Updater) Checking plugin updated for ${plugins.size} plugins...") + + val duration = measureTimeMillis { + plugins.forEach { checkAndUpdate(it) } + } + + println("$LOG_PREFIX (Updater) Update check completed in ${duration}ms") + } + + private suspend fun checkAndUpdate(plugin: UpdatablePlugin) = withContext(Dispatchers.IO) { + if (cooldownTracker.isOnCooldown(plugin.name)) { + return@withContext + } + + val release = gitHubClient.fetchLatestRelease(plugin) ?: return@withContext + + val latestVersion = release["tag_name"]?.toString()?.trimStart('v') ?: return@withContext + + if (!isNewer(latestVersion, plugin.currentVersion)) { + return@withContext + } + + @Suppress("UNCHECKED_CAST") + val assets = release["assets"] as? List> ?: return@withContext + + val matchingAsset = assets.firstOrNull { asset -> + val assetName = asset["name"]?.toString() ?: return@firstOrNull false + assetName.endsWith(".jar") && assetName.contains(plugin.findPluginType()) + } ?: run { + return@withContext + } + + val downloadUrl = matchingAsset["browser_download_url"]?.toString() ?: return@withContext + val assetName = matchingAsset["name"]?.toString() ?: return@withContext + + backupOldJar(plugin) + + gitHubClient.downloadAsset(downloadUrl, pluginsPath.resolve(assetName)) + cooldownTracker.markUpdated(plugin.name) + + println("$LOG_PREFIX (Updater) Updated ${plugin.name} from ${plugin.currentVersion} to $latestVersion") + } + + private fun backupOldJar(plugin: UpdatablePlugin) { + if (!Files.exists(plugin.jarPath)) { + return + } + + Files.move( + plugin.jarPath, + oldPath.resolve("${plugin.name}-${plugin.currentVersion}.jar"), + StandardCopyOption.REPLACE_EXISTING + ) + } + + private fun isNewer(latest: String, current: String): Boolean { + fun split(v: String): Pair, String?> { + val parts = v.split("-", limit = 2) + val nums = parts[0].split(".").map { it.toIntOrNull() ?: 0 } + val suffix = parts.getOrNull(1)?.uppercase() + return nums to suffix + } + + fun rank(suffix: String?): Int { + if (suffix == null) return 3 + return when { + suffix.contains("SNAPSHOT") -> 0 + suffix.contains("ALPHA") -> 1 + suffix.contains("BETA") -> 2 + else -> 1 + } + } + + val (lNums, lSuffix) = split(latest) + val (cNums, cSuffix) = split(current) + + for (i in 0 until maxOf(lNums.size, cNums.size)) { + val diff = lNums.getOrElse(i) { 0 } - cNums.getOrElse(i) { 0 } + if (diff != 0) return diff > 0 + } + + return rank(lSuffix) > rank(cSuffix) + } +} \ No newline at end of file From fb103bf658bc3d95ef55fb7d1a4b3a486504c2e5 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Sun, 24 May 2026 16:44:07 +0200 Subject: [PATCH 07/25] refactor: clean up CoreLauncher and GitHubClient for improved readability and error handling --- .../surf/core/launcher/server/CoreLauncher.kt | 9 +----- .../server/updater/github/GitHubClient.kt | 30 ++++++++----------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index ac76b3e1..2fa44452 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -31,7 +31,7 @@ object CoreLauncher { println("$LOG_PREFIX No GitHub personal access token provided, skipping plugin update check") return@withTimeoutOrNull } - + PluginUpdater.start() } ?: println("$LOG_PREFIX Plugin update check timed out after 30 seconds, continuing with server startup") @@ -50,7 +50,6 @@ object CoreLauncher { println("$LOG_PREFIX Server process started") monitorJob = scope.launch { - launch { serverProcess.inputStream.bufferedReader().forEachLine { line -> println(line) @@ -66,12 +65,6 @@ object CoreLauncher { } } - launch { - serverProcess.errorStream.bufferedReader().forEachLine { line -> - println(line) - } - } - MinecraftServerPinger.monitor(serverProcess) } } diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt index 4679bb9a..ae562e22 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt @@ -10,27 +10,21 @@ import java.nio.file.Path import java.nio.file.StandardCopyOption class GitHubClient(private val token: String?) { - private val mapper = jacksonObjectMapper() - fun fetchLatestRelease(plugin: UpdatablePlugin): Map? { - return try { - val connection = openConnection(plugin.latestReleaseUrl, "application/vnd.github+json") - connection.setRequestProperty("X-GitHub-Api-Version", "2022-11-28") - connection.connect() - - if (connection.responseCode != 200) { - println("[GitHub] API antwortete mit ${connection.responseCode} für ${plugin.name}") - return null - } - - connection.inputStream.use { input -> - mapper.readValue(input, object : TypeReference>() {}) - } - } catch (e: Exception) { - null + fun fetchLatestRelease(plugin: UpdatablePlugin): Map? = runCatching { + val connection = openConnection(plugin.latestReleaseUrl, "application/vnd.github+json") + connection.setRequestProperty("X-GitHub-Api-Version", "2022-11-28") + connection.connect() + + if (connection.responseCode != 200) { + return null } - } + + connection.inputStream.use { input -> + mapper.readValue(input, object : TypeReference>() {}) + } + }.getOrNull() fun downloadAsset(url: String, target: Path) { val connection = openConnection(url, "application/octet-stream") From 818fdba9d9ecc17c8c2d0f5f5648436c90ff7213 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:12:04 +0200 Subject: [PATCH 08/25] feat: enhance plugin updater with improved asset matching and special case handling for plugin names --- .../server/updater/UpdatablePlugin.kt | 12 +++++++--- .../server/updater/process/PluginUpdater.kt | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt index 657fec26..ac53277f 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt @@ -17,13 +17,19 @@ data class UpdatablePlugin( /** * The Plugin name, e.g. core, captcha or api */ - fun findPluginName() = - name.substringAfter("surf-").substringBefore("-paper").substringBefore("-velocity") + fun findPluginName(): String { + if (name == "surf-paper-api") { // Special case for the old API plugin name + return "api" + } + + return name.substringAfter("surf-").substringBefore("-paper").substringBefore("-velocity") + .replace("-server", "").replace("-api", "") + } fun findPluginType() = when { name.contains("-paper") -> "paper" name.contains("-velocity") -> "velocity" - else -> "velocity" + else -> null } val latestReleaseUrl get() = "https://api.github.com/repos/SLNE-DEVELOPMENT/surf-${findPluginName()}/releases/latest" diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt index ac8ad6f1..23dadc55 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -14,7 +14,7 @@ import kotlin.system.measureTimeMillis object PluginUpdater { private val pluginsPath = Path.of("plugins") - private val oldPath = pluginsPath.resolve("old") + private val oldPath = pluginsPath.resolve(".old") private val scanner = PluginScanner(pluginsPath) private val gitHubClient = GitHubClient(CoreLauncherConfig.getConfig().personalAccessToken) @@ -28,10 +28,12 @@ object PluginUpdater { } withContext(Dispatchers.IO) { - if (!Files.exists(oldPath)) Files.createDirectories(oldPath) + if (!Files.exists(oldPath)) { + Files.createDirectories(oldPath) + } } - println("$LOG_PREFIX (Updater) Checking plugin updated for ${plugins.size} plugins...") + println("$LOG_PREFIX (Updater) Checking plugin updates for ${plugins.size} plugins...") val duration = measureTimeMillis { plugins.forEach { checkAndUpdate(it) } @@ -56,10 +58,23 @@ object PluginUpdater { @Suppress("UNCHECKED_CAST") val assets = release["assets"] as? List> ?: return@withContext + val expectedPrefix = buildString { + append("surf-") + append(plugin.findPluginName()) + + plugin.findPluginType()?.let { + append("-") + append(plugin.findPluginType()) + } + } + val matchingAsset = assets.firstOrNull { asset -> val assetName = asset["name"]?.toString() ?: return@firstOrNull false - assetName.endsWith(".jar") && assetName.contains(plugin.findPluginType()) + + assetName.endsWith(".jar") && + assetName.startsWith(expectedPrefix) } ?: run { + println("$LOG_PREFIX (Updater) No matching asset found for ${plugin.name} in release $latestVersion") return@withContext } From bdb5c51d4f5b27b3ecd37ee54b82812b145f0781 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:17:09 +0200 Subject: [PATCH 09/25] feat: optimize plugin update checks using coroutines for improved performance --- .../launcher/server/updater/process/PluginUpdater.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt index 23dadc55..32e50a4e 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -5,8 +5,7 @@ import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin import dev.slne.surf.core.launcher.server.updater.cooldown.UpdateCooldownTracker import dev.slne.surf.core.launcher.server.updater.github.GitHubClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption @@ -36,7 +35,13 @@ object PluginUpdater { println("$LOG_PREFIX (Updater) Checking plugin updates for ${plugins.size} plugins...") val duration = measureTimeMillis { - plugins.forEach { checkAndUpdate(it) } + coroutineScope { + plugins.map { plugin -> + async(Dispatchers.IO) { + checkAndUpdate(plugin) + } + }.awaitAll() + } } println("$LOG_PREFIX (Updater) Update check completed in ${duration}ms") From 303298ea7e7ff59517d22bb96ed71b080a6b0ed7 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:32:28 +0200 Subject: [PATCH 10/25] feat: update log prefix in CoreLauncher to include timestamp for better debugging --- .../dev/slne/surf/core/launcher/server/CoreLauncher.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 2fa44452..41f5f2fe 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -1,16 +1,22 @@ package dev.slne.surf.core.launcher.server +import dev.slne.surf.api.core.util.dateTimeFormatter import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger import dev.slne.surf.core.launcher.server.updater.process.PluginUpdater import kotlinx.coroutines.* +import java.time.LocalDateTime import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration.Companion.seconds -const val LOG_PREFIX = "\u001B[0;91m[CoreLauncher]\u001B[0m" +val LOG_PREFIX + get() = + "\u001B[0;91m${ + LocalDateTime.now().format(dateTimeFormatter) + }\u001B[0m \u001B[0;91m[CoreLauncher]\u001B[0m" object CoreLauncher { private val shuttingDown = AtomicBoolean(false) From 6c10e6f1a0fd0c0af319d93bf5856c768ca7cc97 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:33:08 +0200 Subject: [PATCH 11/25] feat: update log prefix in CoreLauncher to use braces for better formatting --- .../kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 41f5f2fe..55a28871 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -14,9 +14,9 @@ import kotlin.time.Duration.Companion.seconds val LOG_PREFIX get() = - "\u001B[0;91m${ + "\u001B[0;91m{${ LocalDateTime.now().format(dateTimeFormatter) - }\u001B[0m \u001B[0;91m[CoreLauncher]\u001B[0m" + }]\u001B[0m \u001B[0;91m[CoreLauncher]\u001B[0m" object CoreLauncher { private val shuttingDown = AtomicBoolean(false) From 11af783d47ef04189d84c83836d33db97895dc8b Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:34:47 +0200 Subject: [PATCH 12/25] feat: update log prefix in CoreLauncher to use a new date-time format for improved readability --- .../dev/slne/surf/core/launcher/server/CoreLauncher.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 55a28871..b54e6230 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -1,6 +1,5 @@ package dev.slne.surf.core.launcher.server -import dev.slne.surf.api.core.util.dateTimeFormatter import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig @@ -8,14 +7,17 @@ import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger import dev.slne.surf.core.launcher.server.updater.process.PluginUpdater import kotlinx.coroutines.* import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.Duration.Companion.seconds +private val secondDateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss") + val LOG_PREFIX get() = "\u001B[0;91m{${ - LocalDateTime.now().format(dateTimeFormatter) + LocalDateTime.now().format(secondDateTimeFormatter) }]\u001B[0m \u001B[0;91m[CoreLauncher]\u001B[0m" object CoreLauncher { From 637bc4d70d36725e0653f6971f3840779f0f77c0 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 16:44:52 +0200 Subject: [PATCH 13/25] feat: update log prefix in CoreLauncher to use square brackets for better formatting --- .../kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index b54e6230..ef4cd1e8 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -16,9 +16,9 @@ private val secondDateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH val LOG_PREFIX get() = - "\u001B[0;91m{${ + "\u001B[0;91m[${ LocalDateTime.now().format(secondDateTimeFormatter) - }]\u001B[0m \u001B[0;91m[CoreLauncher]\u001B[0m" + } CoreLauncher]\u001B[0m" object CoreLauncher { private val shuttingDown = AtomicBoolean(false) From 77c602641d0cdc4b92de67e14ede31290606e539 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Mon, 25 May 2026 21:36:07 +0200 Subject: [PATCH 14/25] feat: refactor CoreLauncher to use lazy config initialization and improve plugin update checks --- .../slne/surf/core/launcher/server/CoreLauncher.kt | 11 +++++++---- .../core/launcher/server/config/CoreLauncherConfig.kt | 3 ++- .../launcher/server/updater/process/PluginScanner.kt | 2 ++ .../launcher/server/updater/process/PluginUpdater.kt | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index ef4cd1e8..9f10b72e 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -26,16 +26,19 @@ object CoreLauncher { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var monitorJob: Job? = null var minecraftServerOnline: Boolean = false + val config by lazy { + CoreLauncherConfig.getConfig() + } suspend fun launch() { SurfApiStandaloneBootstrap.bootstrap() SurfApiStandaloneBootstrap.enable() - if (CoreLauncherConfig.getConfig().autoUpdateSurfPlugins) { + if (config.autoUpdateSurfPlugins) { println("$LOG_PREFIX Searching plugin updates...") withTimeoutOrNull(20.seconds) { - if (CoreLauncherConfig.getConfig().personalAccessToken.isBlank()) { + if (config.personalAccessToken.isBlank()) { println("$LOG_PREFIX No GitHub personal access token provided, skipping plugin update check") return@withTimeoutOrNull } @@ -63,7 +66,7 @@ object CoreLauncher { println(line) if (line.contains( - CoreLauncherConfig.getConfig().startedMessage, + config.startedMessage, ignoreCase = true ) ) { @@ -96,7 +99,7 @@ object CoreLauncher { fun isShuttingDown(): Boolean = shuttingDown.get() private fun buildStartupCommand(): List { - val base = CoreLauncherConfig.getConfig().serverStartupCommand + val base = config.serverStartupCommand val parts = Regex("""[^\s"]+|"([^"]*)"""") .findAll(base) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt index 5f0a820f..1b8f885b 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt @@ -15,7 +15,8 @@ data class CoreLauncherConfig( val serverCategory: String = "unknown", val serverUuid: SerializableUUID = UUID.randomUUID(), val autoUpdateSurfPlugins: Boolean = true, - val personalAccessToken: String = "" + val personalAccessToken: String = "", + val autoUpdateIgnoredPlugins: List = listOf("surf-example-paper") ) { companion object : SpongeYmlConfigClass( CoreLauncherConfig::class.java, diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt index 0cd8318e..869dd17f 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt @@ -1,5 +1,6 @@ package dev.slne.surf.core.launcher.server.updater.process +import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -24,6 +25,7 @@ class PluginScanner(private val pluginsPath: Path) { .filter { it.name.startsWith("surf-") } .toList() .mapNotNull { readPlugin(it) } + .filter { it.name !in CoreLauncher.config.autoUpdateIgnoredPlugins } .toList() } } diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt index 32e50a4e..971f5ebe 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -1,7 +1,7 @@ package dev.slne.surf.core.launcher.server.updater.process +import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.LOG_PREFIX -import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin import dev.slne.surf.core.launcher.server.updater.cooldown.UpdateCooldownTracker import dev.slne.surf.core.launcher.server.updater.github.GitHubClient @@ -16,7 +16,7 @@ object PluginUpdater { private val oldPath = pluginsPath.resolve(".old") private val scanner = PluginScanner(pluginsPath) - private val gitHubClient = GitHubClient(CoreLauncherConfig.getConfig().personalAccessToken) + private val gitHubClient = GitHubClient(CoreLauncher.config.personalAccessToken) private val cooldownTracker = UpdateCooldownTracker(pluginsPath.resolve(".last-updates")) suspend fun start() { From 3196f48f2ff145b86807fc5fbe84a037082ec64c Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Tue, 26 May 2026 02:04:07 +0200 Subject: [PATCH 15/25] feat: add support for core launcher integration and update event handling --- surf-core-api/surf-core-api-common/build.gradle.kts | 4 ++++ .../api/common/event/redis}/SurfEventFireRedisEvent.kt | 2 +- .../core/core/common/event/LocalSurfEventBusListener.kt | 2 +- .../dev/slne/surf/core/core/common/event/SurfEventBus.kt | 2 +- .../dev/slne/surf/core/launcher/server/CoreLauncher.kt | 2 +- surf-core-paper/build.gradle.kts | 1 + .../kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt | 8 ++++++-- surf-core-velocity/build.gradle.kts | 1 + .../kotlin/dev/slne/surf/core/velocity/VelocityMain.kt | 6 +++++- .../dev/slne/surf/core/velocity/command/CoreCommand.kt | 5 +++++ 10 files changed, 26 insertions(+), 7 deletions(-) rename {surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event => surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis}/SurfEventFireRedisEvent.kt (81%) diff --git a/surf-core-api/surf-core-api-common/build.gradle.kts b/surf-core-api/surf-core-api-common/build.gradle.kts index e170c8a2..87e51963 100644 --- a/surf-core-api/surf-core-api-common/build.gradle.kts +++ b/surf-core-api/surf-core-api-common/build.gradle.kts @@ -1,3 +1,7 @@ plugins { id("dev.slne.surf.api.gradle.core") +} + +surfCoreApi { + withSurfRedis() } \ No newline at end of file diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt similarity index 81% rename from surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt rename to surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt index 7edc03f0..8d203bd4 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.core.core.common.redis.event +package dev.slne.surf.core.api.common.event.redis import dev.slne.surf.core.api.common.event.SurfEvent import dev.slne.surf.redis.event.RedisEvent diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt index 6e78bb17..e59e3966 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt @@ -1,6 +1,6 @@ package dev.slne.surf.core.core.common.event -import dev.slne.surf.core.core.common.redis.event.SurfEventFireRedisEvent +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent import dev.slne.surf.redis.event.OnRedisEvent object LocalSurfEventBusListener { diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt index 82bcd76d..2248af73 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt @@ -2,8 +2,8 @@ package dev.slne.surf.core.core.common.event import dev.slne.surf.core.api.common.event.SurfEvent import dev.slne.surf.core.api.common.event.SurfEventHandler +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent import dev.slne.surf.core.core.CoreInstance -import dev.slne.surf.core.core.common.redis.event.SurfEventFireRedisEvent import kotlin.reflect.KClass import kotlin.reflect.full.declaredFunctions import kotlin.reflect.full.findAnnotation diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 9f10b72e..ecde802f 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -106,7 +106,7 @@ object CoreLauncher { .map { it.value.replace("\"", "") } .toMutableList() - val flag = "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}=true" + val flag = "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}" if (parts.none { it == flag }) { parts.add(1, flag) diff --git a/surf-core-paper/build.gradle.kts b/surf-core-paper/build.gradle.kts index ef5256b3..542fc08b 100644 --- a/surf-core-paper/build.gradle.kts +++ b/surf-core-paper/build.gradle.kts @@ -27,4 +27,5 @@ surfPaperPluginApi { dependencies { api(projects.surfCoreCore.surfCoreCorePaper) compileOnly("net.luckperms:api:5.4") + implementation(projects.surfCoreLauncher.surfCoreLauncherApi) } \ No newline at end of file diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt index 3dbca9c0..1f9fb986 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt @@ -9,6 +9,7 @@ import dev.slne.surf.core.core.CoreInstance import dev.slne.surf.core.core.common.config.SurfServerConfiguration import dev.slne.surf.core.core.common.event.SurfEventBus import dev.slne.surf.core.core.common.server.SurfServerService +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.paper.api.DefaultCorePlayerInfoProvider import dev.slne.surf.core.paper.redis.listener.PaperRedisListener import dev.slne.surf.core.paper.teleport.TeleportRedisListener @@ -46,9 +47,12 @@ class PaperBootstrap : PluginBootstrap { startedAt = OffsetDateTime.now() ) - SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) - SurfServerService.addServer(server) + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) == null) { + SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + } + + SurfServerService.addServer(server) CorePlayerInfoProvider.setInstance(DefaultCorePlayerInfoProvider) } diff --git a/surf-core-velocity/build.gradle.kts b/surf-core-velocity/build.gradle.kts index 25f15c05..7683b16c 100644 --- a/surf-core-velocity/build.gradle.kts +++ b/surf-core-velocity/build.gradle.kts @@ -17,4 +17,5 @@ velocityPluginFile { dependencies { api(projects.surfCoreCore.surfCoreCoreVelocity) + implementation(projects.surfCoreLauncher.surfCoreLauncherApi) } \ No newline at end of file diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt index db21f718..246bf93f 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt @@ -25,6 +25,7 @@ import dev.slne.surf.core.core.common.event.SurfEventBus import dev.slne.surf.core.core.common.server.SurfServerService import dev.slne.surf.core.core.common.util.appendCorePrefix import dev.slne.surf.core.core.common.util.niceRed +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.velocity.auth.AuthenticationListener import dev.slne.surf.core.velocity.auth.AuthenticationService import dev.slne.surf.core.velocity.command.coreCommand @@ -83,7 +84,10 @@ class VelocityMain @Inject constructor( ) ) - SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) == null) { + SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + } + SurfServerService.addServer(server) } diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt index f6c1fa6f..5c893253 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt @@ -20,6 +20,7 @@ import dev.slne.surf.core.core.common.player.SurfPlayerService import dev.slne.surf.core.core.common.server.SurfServerService import dev.slne.surf.core.core.common.util.appendCorePrefix import dev.slne.surf.core.core.common.util.niceRed +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.velocity.permission.PermissionList import dev.slne.surf.core.velocity.plugin import net.kyori.adventure.text.Component @@ -52,6 +53,10 @@ fun coreCommand() = commandTree("core") { info(" by ") variableValue(vendor) info(".") + + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) != null) { + spacer(" (and is launched by the core launcher)") + } } } From 67a39722f10d324fe0402f331ce383dfdc83ccde Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Tue, 26 May 2026 02:05:00 +0200 Subject: [PATCH 16/25] feat: add surfCoreApiCommon dependency to build configuration --- surf-core-launcher/surf-core-launcher-server/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 4ccecb06..11a27ff5 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") + implementation(projects.surfCoreApi.surfCoreApiCommon) } tasks.shadowJar { From 5dff79a2743f23e56f20d85941922c7489de57e5 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Tue, 26 May 2026 10:35:38 +0200 Subject: [PATCH 17/25] feat: enhance VelocityRedisListener with service status handling and caching --- .../common/server/state/SurfServiceStatus.kt | 2 - .../build.gradle.kts | 2 +- .../redis/listener/VelocityRedisListener.kt | 44 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt index 05889e23..711284a0 100644 --- a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt @@ -1,8 +1,6 @@ package dev.slne.surf.core.api.common.server.state enum class SurfServiceStatus { - LAUNCHING, - ONLINE, UNREACHABLE, CRASHED } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 11a27ff5..4d187022 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -4,9 +4,9 @@ plugins { dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) + implementation(projects.surfCoreApi.surfCoreApiCommon) implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") - implementation(projects.surfCoreApi.surfCoreApiCommon) } tasks.shadowJar { diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index bc2da92b..65b22925 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -1,11 +1,18 @@ package dev.slne.surf.core.velocity.redis.listener +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.expireAfterWrite +import dev.slne.surf.api.core.messages.adventure.sendText +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus import dev.slne.surf.core.core.common.redis.event.SurfPlayerMessageRedisEvent import dev.slne.surf.core.core.common.redis.event.SurfPlayerResyncRedisEvent +import dev.slne.surf.core.core.common.util.appendCorePrefix +import dev.slne.surf.core.launcher.api.redis.ServiceStatusRedisEvent import dev.slne.surf.core.velocity.plugin import dev.slne.surf.core.velocity.task.surfPlayerSyncTask import dev.slne.surf.redis.event.OnRedisEvent import kotlin.jvm.optionals.getOrNull +import kotlin.time.Duration.Companion.seconds object VelocityRedisListener { @OnRedisEvent @@ -17,4 +24,41 @@ object VelocityRedisListener { fun onSurfPlayerResync(event: SurfPlayerResyncRedisEvent) { surfPlayerSyncTask.syncPlayers() } + + private val instableConnectionCache = Caffeine.newBuilder() + .expireAfterWrite(10.seconds) + .build() + + @OnRedisEvent + fun onServiceStatus(event: ServiceStatusRedisEvent) { + val status = event.status + val cachedAmount = + instableConnectionCache.asMap().values.filter { it.status == status }.size + + if (cachedAmount < 2) { + plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + it.sendText { + appendCorePrefix() + info("Der Server ") + variableValue(event.serviceName) + + when (status) { + SurfServiceStatus.UNREACHABLE -> info("hat derzeit Verbindungsprobleme!") + SurfServiceStatus.CRASHED -> { + info("hat die Verbindung verloren! (CRASH?)") + } + } + } + } + } else { + plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + it.sendText { + appendCorePrefix() + info("Der Server ") + variableValue(event.serviceName) + info(" hat derzeit Verbindungsprobleme! Check server logs. (x$cachedAmount $status)") + } + } + } + } } \ No newline at end of file From 3682ce4ccbe80c69d01c83777c6c0519fe6efa5e Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Tue, 26 May 2026 10:55:41 +0200 Subject: [PATCH 18/25] feat: add toggle command for service status message notifications --- .../surf/core/velocity/command/CoreCommand.kt | 24 +++++++++++++++++++ .../velocity/permission/PermissionList.kt | 3 +++ .../redis/listener/VelocityRedisListener.kt | 17 ++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt index 5c893253..1273ae8f 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt @@ -23,6 +23,7 @@ import dev.slne.surf.core.core.common.util.niceRed import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.velocity.permission.PermissionList import dev.slne.surf.core.velocity.plugin +import dev.slne.surf.core.velocity.redis.listener.VelocityRedisListener import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.TextDecoration @@ -60,6 +61,29 @@ fun coreCommand() = commandTree("core") { } } + literalArgument("togglecoreservicestatusmessages") { + withPermission(PermissionList.CORE_COMMAND_TOGGLE_SERVICE_STATUS_MESSAGES) + + playerExecutor { player, _ -> + val current = VelocityRedisListener.ignoringPlayers.contains(player.uniqueId) + + if (current) { + VelocityRedisListener.ignoringPlayers.remove(player.uniqueId) + } else { + VelocityRedisListener.ignoringPlayers.add(player.uniqueId) + } + + player.sendText { + appendCorePrefix() + if (current) { + success("Du erhältst nun wieder Service Statusnachrichten.") + } else { + success("Du erhältst nun keine Service Statusnachrichten mehr.") + } + } + } + } + literalArgument("player") { withPermission(PermissionList.CORE_COMMAND_PLAYER) diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt index 591e73fd..cd5b122a 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt @@ -8,4 +8,7 @@ object PermissionList { const val CORE_COMMAND_PLAYER = "$CORE_COMMAND.player" const val CORE_COMMAND_SERVICE = "$CORE_COMMAND.service" + + val CORE_COMMAND_TOGGLE_SERVICE_STATUS_MESSAGES = + "$CORE_COMMAND.togglecoreservicestatusmessages" } \ No newline at end of file diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index 65b22925..c8cfb459 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -3,6 +3,7 @@ package dev.slne.surf.core.velocity.redis.listener import com.github.benmanes.caffeine.cache.Caffeine import com.sksamuel.aedile.core.expireAfterWrite import dev.slne.surf.api.core.messages.adventure.sendText +import dev.slne.surf.api.core.util.random import dev.slne.surf.core.api.common.server.state.SurfServiceStatus import dev.slne.surf.core.core.common.redis.event.SurfPlayerMessageRedisEvent import dev.slne.surf.core.core.common.redis.event.SurfPlayerResyncRedisEvent @@ -11,6 +12,8 @@ import dev.slne.surf.core.launcher.api.redis.ServiceStatusRedisEvent import dev.slne.surf.core.velocity.plugin import dev.slne.surf.core.velocity.task.surfPlayerSyncTask import dev.slne.surf.redis.event.OnRedisEvent +import java.util.* +import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration.Companion.seconds @@ -27,7 +30,9 @@ object VelocityRedisListener { private val instableConnectionCache = Caffeine.newBuilder() .expireAfterWrite(10.seconds) - .build() + .build() + + val ignoringPlayers: ConcurrentHashMap.KeySetView = ConcurrentHashMap.newKeySet() @OnRedisEvent fun onServiceStatus(event: ServiceStatusRedisEvent) { @@ -37,6 +42,10 @@ object VelocityRedisListener { if (cachedAmount < 2) { plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + if (ignoringPlayers.contains(it.uniqueId)) { + return@forEach + } + it.sendText { appendCorePrefix() info("Der Server ") @@ -52,6 +61,10 @@ object VelocityRedisListener { } } else { plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + if (ignoringPlayers.contains(it.uniqueId)) { + return@forEach + } + it.sendText { appendCorePrefix() info("Der Server ") @@ -60,5 +73,7 @@ object VelocityRedisListener { } } } + + instableConnectionCache.put(random.nextInt(), event) } } \ No newline at end of file From f69886470a18520a7eabff185a8075ac3f89b422 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Tue, 26 May 2026 14:45:34 +0200 Subject: [PATCH 19/25] feat: integrate Redis for service status monitoring and update handling --- .../api/redis/ServiceStatusRedisEvent.kt | 3 +- .../build.gradle.kts | 11 ++-- .../surf/core/launcher/server/CoreLauncher.kt | 60 ++++++++++++++++++- .../server/ping/MinecraftServerPinger.kt | 50 +++++++++++++--- .../core/paper/command/SurfCoreCommand.kt | 32 ++++++++++ .../redis/listener/VelocityRedisListener.kt | 10 ++-- 6 files changed, 144 insertions(+), 22 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt index 1f480841..4ea3e20d 100644 --- a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt +++ b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt @@ -1,10 +1,11 @@ package dev.slne.surf.core.launcher.api.redis import dev.slne.surf.core.api.common.server.state.SurfServiceStatus +import dev.slne.surf.redis.event.RedisEvent import kotlinx.serialization.Serializable @Serializable data class ServiceStatusRedisEvent( val serviceName: String, val status: SurfServiceStatus -) +) : RedisEvent() diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts index 4d187022..5e8c3527 100644 --- a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -5,16 +5,17 @@ plugins { dependencies { api(projects.surfCoreLauncher.surfCoreLauncherApi) implementation(projects.surfCoreApi.surfCoreApiCommon) - implementation("dev.slne.surf.redis:surf-redis-api:1.6.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") -} - -tasks.shadowJar { - relocate("dev.slne.surf.redis", "dev.slne.surf.core.launcher.libs") + implementation("dev.slne.surf.redis:surf-redis-standalone:1.6.1") } tasks.jar { manifest { attributes["Main-Class"] = "dev.slne.surf.core.launcher.server.CoreLauncherKt" } +} + +tasks.shadowJar { + exclude("okio/**") + exclude("io/netty/**") } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index ecde802f..8b31a058 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -1,15 +1,22 @@ package dev.slne.surf.core.launcher.server import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap +import dev.slne.surf.core.api.common.event.SurfServerStartEvent +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger import dev.slne.surf.core.launcher.server.updater.process.PluginUpdater +import dev.slne.surf.redis.RedisApi +import dev.slne.surf.redis.StandaloneRedisInstance import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import java.nio.file.Path import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.Path import kotlin.time.Duration.Companion.seconds private val secondDateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss") @@ -23,17 +30,34 @@ val LOG_PREFIX object CoreLauncher { private val shuttingDown = AtomicBoolean(false) lateinit var serverProcess: Process + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private var monitorJob: Job? = null - var minecraftServerOnline: Boolean = false + private val redisInstance = StandaloneRedisInstance( + name = "surf-core-launcher", + configPath = findRedisPluginPath() + ?: error("Could not find Redis plugin configuration path, cannot start Redis instance") + ) + lateinit var redisApi: RedisApi + + val serverOnline = MutableStateFlow(false) + val config by lazy { CoreLauncherConfig.getConfig() } - suspend fun launch() { + suspend fun launch() = withContext(Dispatchers.IO) { SurfApiStandaloneBootstrap.bootstrap() SurfApiStandaloneBootstrap.enable() + println("$LOG_PREFIX Initializing Redis instance...") + + redisInstance.create() + redisApi = RedisApi.create() + redisApi.freezeAndConnect() + + println("$LOG_PREFIX Redis instance initialized and connected") + if (config.autoUpdateSurfPlugins) { println("$LOG_PREFIX Searching plugin updates...") @@ -60,6 +84,14 @@ object CoreLauncher { println("$LOG_PREFIX Server process started") + redisApi.publishEvent( + SurfEventFireRedisEvent( + SurfServerStartEvent( + serverName = config.serverName + ) + ) + ) + monitorJob = scope.launch { launch { serverProcess.inputStream.bufferedReader().forEachLine { line -> @@ -70,7 +102,7 @@ object CoreLauncher { ignoreCase = true ) ) { - minecraftServerOnline = true + serverOnline.value = true println("$LOG_PREFIX Server is now online.") } } @@ -81,7 +113,16 @@ object CoreLauncher { } suspend fun shutdown() { + if (!shuttingDown.compareAndSet(false, true)) { + return + } + println("$LOG_PREFIX Shutting down launcher/server...") + println("$LOG_PREFIX Disconnecting Redis instance...") + + redisApi.disconnect() + redisInstance.shutdown() + println("$LOG_PREFIX Redis instance disconnected and shutdown") monitorJob?.cancelAndJoin() @@ -114,6 +155,17 @@ object CoreLauncher { return parts } + + private fun findRedisPluginPath(): Path? { + val possiblePaths = listOf( + Path("plugins", "surf-redis-paper"), + Path("plugins", "surf-redis-velocity") + ) + + return possiblePaths.firstOrNull { path -> + path.toFile().exists() + } + } } suspend fun main(args: Array) { @@ -126,5 +178,7 @@ suspend fun main(args: Array) { CoreLauncher.launch() CoreLauncher.serverProcess.waitFor() + + CoreLauncher.shutdown() SurfApiStandaloneBootstrap.shutdown() } \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt index 0dd37b6e..49a28398 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -1,9 +1,12 @@ package dev.slne.surf.core.launcher.server.ping +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus +import dev.slne.surf.core.launcher.api.redis.ServiceStatusRedisEvent import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.CoreLauncherEnvironment import dev.slne.surf.core.launcher.server.LOG_PREFIX import kotlinx.coroutines.* +import kotlinx.coroutines.flow.first import java.io.DataInputStream import java.io.DataOutputStream import java.net.InetSocketAddress @@ -22,12 +25,24 @@ object MinecraftServerPinger { println("$LOG_PREFIX Minecraft server stopped") } else { println("$LOG_PREFIX Minecraft server crashed!") + CoreLauncher.redisApi.publishEvent( + ServiceStatusRedisEvent( + serviceName = CoreLauncher.config.serverName, + status = SurfServiceStatus.CRASHED + ) + ) } } launch { - while (process.isAlive && CoreLauncher.minecraftServerOnline) { - delay(5.seconds) + CoreLauncher.serverOnline.first { it } + + if (!process.isAlive) { + return@launch + } + + while (process.isAlive && !CoreLauncher.isShuttingDown()) { + delay(3.seconds) val reachable = withContext(Dispatchers.IO) { isMinecraftReady(CoreLauncherEnvironment.SERVER_PORT) @@ -35,6 +50,12 @@ object MinecraftServerPinger { if (!reachable) { println("$LOG_PREFIX Minecraft server is unreachable") + CoreLauncher.redisApi.publishEvent( + ServiceStatusRedisEvent( + serviceName = CoreLauncher.config.serverName, + status = SurfServiceStatus.UNREACHABLE + ) + ) } } } @@ -51,17 +72,30 @@ object MinecraftServerPinger { DataOutputStream(socket.getOutputStream()).use { output -> DataInputStream(socket.getInputStream()).use { input -> - val handshake = buildHandshakePacket(host, port) - - output.write(handshake) - output.write(byteArrayOf(0x01, 0x00)) + output.write(buildHandshakePacket(host, port)) + output.write(byteArrayOf(0x01, 0x00)) // Status request output.flush() - input.readByte() + readVarInt(input) // Packet length + val packetId = readVarInt(input) // Packet ID - true + packetId == 0x00 // 0x00 = gültiger Status Response } } } }.getOrDefault(false) + + private fun readVarInt(input: DataInputStream): Int { + var value = 0 + var position = 0 + while (true) { + val byte = input.readByte().toInt() + value = value or ((byte and 0x7F) shl position) + if (byte and 0x80 == 0) break + position += 7 + if (position >= 35) error("VarInt too large") + } + return value + } + } \ No newline at end of file diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt index 76a757e0..04efcf41 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt @@ -48,6 +48,38 @@ fun surfCoreCommand() = commandTree("surfcore") { } } +// literalArgument("crash") { +// withRequirement { +// SurfServer.current().name.contains("dev") +// } +// +// anyExecutor { executor, _ -> +// executor.sendText { +// appendCorePrefix() +// error("Der Server wird nun absichtlich zum Testen von Crash-Handling-Funktionen abstürzen...") +// } +// +// Runtime.getRuntime().halt(1) +// } +// } +// +// literalArgument("unreachable") { +// withRequirement { +// SurfServer.current().name.contains("dev") +// } +// +// anyExecutor { executor, _ -> +// executor.sendText { +// appendCorePrefix() +// error("Der Server wird nun absichtlich unerreichbar gemacht, um die Handhabung von Verbindungsproblemen zu testen... o7") +// } +// +// while (true) { +// Thread.sleep(1000) +// } +// } +// } + literalArgument("testawaitingsend") { literalArgument("server") { surfBackendServerArgument("backend") { diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index c8cfb459..70670270 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -48,13 +48,13 @@ object VelocityRedisListener { it.sendText { appendCorePrefix() - info("Der Server ") + error("Der Server ") variableValue(event.serviceName) when (status) { - SurfServiceStatus.UNREACHABLE -> info("hat derzeit Verbindungsprobleme!") + SurfServiceStatus.UNREACHABLE -> error(" hat derzeit Verbindungsprobleme!") SurfServiceStatus.CRASHED -> { - info("hat die Verbindung verloren! (CRASH?)") + error(" hat die Verbindung verloren! (CRASH?)") } } } @@ -67,9 +67,9 @@ object VelocityRedisListener { it.sendText { appendCorePrefix() - info("Der Server ") + error("Der Server ") variableValue(event.serviceName) - info(" hat derzeit Verbindungsprobleme! Check server logs. (x$cachedAmount $status)") + error(" hat derzeit Verbindungsprobleme! Check server logs. (x$cachedAmount $status)") } } } From 372e0d5913305bc4bb9a57a3bf0dc6f9268df4a9 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Wed, 27 May 2026 17:14:35 +0200 Subject: [PATCH 20/25] feat: add publishing configuration for slneReleases repository --- .../surf-core-launcher-api/build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/surf-core-launcher/surf-core-launcher-api/build.gradle.kts b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts index 0d542d6c..2654a79c 100644 --- a/surf-core-launcher/surf-core-launcher-api/build.gradle.kts +++ b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts @@ -1,3 +1,5 @@ +import dev.slne.surf.api.gradle.util.slneReleases + plugins { id("dev.slne.surf.api.gradle.core") } @@ -8,4 +10,10 @@ surfCoreApi { dependencies { api(projects.surfCoreApi.surfCoreApiCommon) +} + +publishing { + repositories { + slneReleases() + } } \ No newline at end of file From 0bf67d84d2a8fdbd097c5ef8ae4df0b9e3dabb62 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft Date: Thu, 28 May 2026 13:09:56 +0200 Subject: [PATCH 21/25] feat: enhance server online status handling and support for velocity-plugin.json --- .../surf/core/launcher/server/CoreLauncher.kt | 2 ++ .../launcher/server/ping/MinecraftServerPinger.kt | 4 ++++ .../server/updater/process/PluginScanner.kt | 15 ++++++++++++--- .../server/updater/process/PluginUpdater.kt | 1 + 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 8b31a058..3b9e3e9a 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -117,6 +117,8 @@ object CoreLauncher { return } + serverOnline.value = true + println("$LOG_PREFIX Shutting down launcher/server...") println("$LOG_PREFIX Disconnecting Redis instance...") diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt index 49a28398..9f84bf8f 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -44,6 +44,10 @@ object MinecraftServerPinger { while (process.isAlive && !CoreLauncher.isShuttingDown()) { delay(3.seconds) + if (!CoreLauncher.serverOnline.value) { + continue + } + val reachable = withContext(Dispatchers.IO) { isMinecraftReady(CoreLauncherEnvironment.SERVER_PORT) } diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt index 869dd17f..b879ceea 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt @@ -1,5 +1,6 @@ package dev.slne.surf.core.launcher.server.updater.process +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import dev.slne.surf.core.launcher.server.CoreLauncher import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin import kotlinx.coroutines.Dispatchers @@ -33,17 +34,25 @@ class PluginScanner(private val pluginsPath: Path) { private fun readPlugin(jarPath: Path): UpdatablePlugin? = runCatching { JarFile(jarPath.toFile()).use { jar -> val entry = jar.entries().asSequence().firstOrNull { - it.name == "velocity-plugin.yml" || + it.name == "velocity-plugin.json" || it.name == "paper-plugin.yml" || it.name == "plugin.yml" } ?: return null jar.getInputStream(entry).use { input -> - val data = yaml.load>(input) ?: return null + val data: Map = when (entry.name) { + "velocity-plugin.json" -> jacksonObjectMapper().readValue( + input, + Map::class.java + ) as Map + + else -> yaml.load(input) ?: return null + } + val version = data["version"]?.toString() ?: return null val name = when (entry.name) { - "velocity-plugin.yml" -> data["id"]?.toString() + "velocity-plugin.json" -> data["id"]?.toString() else -> data["name"]?.toString() } ?: return null diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt index 971f5ebe..8ccf89e3 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -23,6 +23,7 @@ object PluginUpdater { val plugins = scanner.findPlugins() if (plugins.isEmpty()) { + println("$LOG_PREFIX (Updater) No plugins found for update checking.") return } From 820b4cbebc0c6bbe67cdf38daa81a57aa0fb5a29 Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com> Date: Sun, 21 Jun 2026 00:53:28 +0200 Subject: [PATCH 22/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt index 3b9e3e9a..862bb96a 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -69,7 +69,7 @@ object CoreLauncher { PluginUpdater.start() } - ?: println("$LOG_PREFIX Plugin update check timed out after 30 seconds, continuing with server startup") + ?: println("$LOG_PREFIX Plugin update check timed out after 20 seconds, continuing with server startup") } println("$LOG_PREFIX Starting Minecraft Server...") From fe3bd789880c46315c5fc660724c1dee26ee071c Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:16:57 +0200 Subject: [PATCH 23/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../surf/core/velocity/redis/listener/VelocityRedisListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index 70670270..be1bdbfa 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -69,7 +69,7 @@ object VelocityRedisListener { appendCorePrefix() error("Der Server ") variableValue(event.serviceName) - error(" hat derzeit Verbindungsprobleme! Check server logs. (x$cachedAmount $status)") + error(" hat derzeit Verbindungsprobleme! Check server logs. (x${cachedAmount + 1} $status)") } } } From 9f1d839b68bb037a2a33e73f961251382b6eabef Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:17:24 +0200 Subject: [PATCH 24/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../core/velocity/redis/listener/VelocityRedisListener.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index be1bdbfa..44f1822a 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -37,9 +37,9 @@ object VelocityRedisListener { @OnRedisEvent fun onServiceStatus(event: ServiceStatusRedisEvent) { val status = event.status - val cachedAmount = - instableConnectionCache.asMap().values.filter { it.status == status }.size - + val cachedAmount = instableConnectionCache.asMap().values.count { + it.status == status && it.serviceName == event.serviceName + } if (cachedAmount < 2) { plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { if (ignoringPlayers.contains(it.uniqueId)) { From 5faed678a845367f72a11710712d0d28da9b90ef Mon Sep 17 00:00:00 2001 From: TheBjoRedCraft <143264463+TheBjoRedCraft@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:17:56 +0200 Subject: [PATCH 25/25] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../surf/core/launcher/server/updater/github/GitHubClient.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt index ae562e22..44f9ef4d 100644 --- a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt @@ -38,6 +38,9 @@ class GitHubClient(private val token: String?) { private fun openConnection(url: String, accept: String): HttpURLConnection { val connection = URI(url).toURL().openConnection() as HttpURLConnection connection.setRequestProperty("Accept", accept) + connection.setRequestProperty("User-Agent", "surf-core-launcher") + connection.connectTimeout = 10_000 + connection.readTimeout = 20_000 token?.let { connection.setRequestProperty("Authorization", "Bearer $it") } return connection }