Files
ForkWar/buildSrc/src/main/kotlin/DevServer.kt
T

291 lines
9.8 KiB
Kotlin

/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File
import java.io.FileInputStream
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.security.MessageDigest
import java.util.Properties
import java.util.Random
import java.util.stream.Collectors
open class DevServer : DefaultTask() {
@get:Input
var debug: Boolean = false
@get:Input
var template: String? = null
@get:Input
@get:Optional
var plugins: String? = null
@get:Input
@get:Optional
var port: Int? = null
@get:Input
@get:Optional
var jar: String? = null
@get:Input
@get:Optional
var dParams: MutableMap<String, String> = HashMap()
@get:Input
@get:Optional
var jvmArgs: String? = null
@get:Input
@get:Optional
var checkpointFolder: String? = null
@get:Input
@get:Optional
var profile: Boolean? = null
@get:Input
@get:Optional
var forceUpgrade: Boolean? = null
@get:Input
@get:Optional
var worldName: String? = null
@get:Internal
var processInput: BufferedWriter? = null
@get:Internal
var host: String? = null
@get:Internal
var debugPort: Int = 0
@get:Internal
var running: Boolean = true
init {
doFirst {
checkpointFolder?.let { dParams.put("checkpoint", it) }
val projects = mutableListOf<Project>()
projects.add(project)
while (projects[0].parent != null) {
projects.add(0, projects[0].parent!!)
}
val properties = Properties()
projects.forEach {
val file = File(it.projectDir, "steamwar.properties")
if (file.exists()) {
FileInputStream(file).use { input -> properties.load(input) }
}
}
if (template!!.startsWith("Bau")) {
if (properties.containsKey("worldName")) {
worldName = properties.getProperty("worldName")
} else {
throw GradleException("Please supply the 'worldName' in a 'steamwar.properties' files either in this project dir or any parent project!")
}
}
host = properties.getProperty("host")
debugPort = Random().nextInt(5001, 10000)
if (host == null) {
throw GradleException("Please supply the 'host' in a 'steamwar.properties' files either in this project dir or any parent project!")
}
}
doLast {
setupTemplate(template!!)
uploadDependencies()
if (debug) startDebugPort()
startDevServer()
}
finalizedBy(Finalizer())
}
inner class Finalizer : DefaultTask() {
init {
doLast {
processInput?.let {
it.write(if (template!!.endsWith("Velocity")) "end\n" else "stop\n")
it.flush()
}
running = false
}
}
}
private fun run(vararg args: String): Process {
val arguments = mutableListOf("ssh", host, "-T")
arguments.addAll(args)
val process = ProcessBuilder(arguments).start()
process.waitFor()
return process
}
private fun checkFileOnRemote(path: String): Boolean {
val process = run("[ -e \"$path\" ] && echo \"true\"")
process.errorStream.close()
process.outputStream.close()
return BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
reader.lines().count() > 0
}
}
private fun closeProcess(process: Process) {
process.outputStream.close()
process.inputStream.close()
process.errorStream.close()
}
fun setupTemplate(template: String) {
if (checkFileOnRemote(template)) return
if (checkFileOnRemote("/configs/GameModes/$template.yml")) {
println("GameMode Config exists")
val process = run("cat /configs/GameModes/$template.yml | grep \"Folder: \"")
val serverTemplateName = BufferedReader(InputStreamReader(process.inputStream)).lines()
.collect(Collectors.joining("\n"))
.trim()
.substring("Folder: ".length)
closeProcess(process)
setupTemplate(serverTemplateName)
run("ln -s $serverTemplateName $template")
return
}
if (!checkFileOnRemote("/servers/$template")) {
throw GradleException("Used template ($template) is not in /servers/ directory of the given host $host")
}
closeProcess(run("cp -r /servers/$template $template"))
closeProcess(run("chmod u+w $template"))
closeProcess(run("rm -r $template/plugins/*WorldEdit/"))
closeProcess(run("rm $template/log4j2.xml"))
}
fun uploadDependencies() {
val base = if (plugins == null) "$template/plugins" else plugins
println("Uploading to ~/$base")
this.dependsOn.forEach {
if (it !is String) {
throw GradleException("Illegal argument for uploading dependencies")
}
val resolved = project.findProject(it.substring(0, it.lastIndexOf(':')))!!
val archiveTask = resolved.tasks.findByName(it.substring(it.lastIndexOf(':') + 1)) as AbstractArchiveTask
val archive = archiveTask.archiveFile.get().asFile
var process = ProcessBuilder("ssh", host, "-T", "sha1sum $base/${archive.name.replace("-all", "")}").start()
val bytes = MessageDigest.getInstance("sha1").digest(archive.readBytes())
val sb = StringBuilder()
for (b in bytes) {
sb.append(String.format("%02X", b))
}
var same = false
process.inputStream.bufferedReader().readLines().forEach { line ->
same = same || line.startsWith(sb.toString().lowercase())
}
closeProcess(process)
if (same) {
println("Skipping $archive")
return@forEach
}
println("Uploading $archive")
process = ProcessBuilder("ssh", host, "-T", "rm $base/${archive.name.replace("-all", "")}").start()
process.waitFor()
closeProcess(process)
process = ProcessBuilder("scp", archive.absolutePath, "$host:~/$base/${archive.name.replace("-all", "")}").start()
process.waitFor()
closeProcess(process)
println("Uploaded $archive")
}
}
fun startDebugPort() {
val process = ProcessBuilder("ssh", host, "-L", "5005:localhost:$debugPort").start()
val processOutput = BufferedReader(InputStreamReader(process.inputStream))
Thread {
while (running) {
}
processOutput.close()
process.errorStream.close()
}.start()
}
fun startDevServer() {
val devPy = StringBuilder().append("sw dev")
if (port != null) devPy.append(" --port $port")
if (worldName != null) devPy.append(" -w $template/$worldName")
if (plugins != null) devPy.append(" -p $plugins")
if (profile != null) devPy.append(" --profile")
if (forceUpgrade != null) devPy.append(" --forceUpgrade")
if (jar != null) devPy.append(" --jar $jar")
devPy.append(" $template")
for ((key, value) in dParams) {
devPy.append(" -D$key=$value")
}
devPy.append(" -Dpaper.disablePluginRemapping=true")
if (debug) devPy.append(" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:$debugPort")
devPy.append(" -javaagent:/jars/AccessWidener.jar=start")
if (jvmArgs != null) devPy.append(" $jvmArgs")
println("Starting $template with command $devPy")
val process = ProcessBuilder("ssh", host, "-T", devPy.toString()).start()
val processOutput = BufferedReader(InputStreamReader(process.inputStream))
Thread {
while (running) {
if (processOutput.ready()) {
println(processOutput.readLine())
}
}
processOutput.close()
process.errorStream.close()
}.start()
processInput = BufferedWriter(OutputStreamWriter(process.outputStream))
val input = BufferedReader(InputStreamReader(System.`in`))
Thread {
while (running) {
val text = input.readLine() ?: break
processInput?.write(text)
processInput?.newLine()
processInput?.flush()
}
}.start()
process.waitFor()
processInput?.close()
processInput = null
running = false
}
}