/* * 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 . */ 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 = 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() 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 } }