All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.robocode.tankroyale.booter.commands.BootCommand.kt Maven / Gradle / Ivy

There is a newer version: 0.26.0
Show newest version
package dev.robocode.tankroyale.booter.commands

import dev.robocode.tankroyale.booter.model.BootEntry
import dev.robocode.tankroyale.booter.util.Env
import dev.robocode.tankroyale.booter.util.OperatingSystemCheck
import dev.robocode.tankroyale.booter.util.OperatingSystemCheck.OperatingSystemType.Mac
import dev.robocode.tankroyale.booter.util.OperatingSystemCheck.OperatingSystemType.Windows
import java.nio.file.Files
import java.nio.file.Files.list
import java.nio.file.Path
import java.util.*
import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.Predicate
import java.util.stream.Collectors.toList
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists

class RunCommand : Command() {

    private val processes = ConcurrentSkipListMap()

    private var teamId: TeamId = 1

    fun boot(bootPaths: Array?) {
        // Kill all running processes before terminating
        Runtime.getRuntime().addShutdownHook(Thread { killAllProcesses() })

        // Start up the bots provided with the input list
        bootPaths?.forEach { createBotProcess(Path(it)) }

        processCommandLineInput()
    }

    private fun processCommandLineInput() {
        while (true) {
            val line = readlnOrNull()?.trim()
            val cmdAndArgs = line?.split("\\s+".toRegex(), limit = 2)
            if (cmdAndArgs?.isNotEmpty() == true) {
                val command = cmdAndArgs[0].lowercase(Locale.getDefault()).trim()
                if (command == "quit") {
                    break // terminate running bots
                }
                if (cmdAndArgs.size >= 2) {
                    val arg = cmdAndArgs[1]
                    when (command) {
                        "boot" -> {
                            val dir = Path(arg)
                            createBotProcess(dir)
                        }
                        "stop" -> {
                            val pid = arg.toLong()
                            stopBotProcess(pid)
                        }
                    }
                }
            }
        }
    }

    private fun createBotProcess(bootDir: Path) {
        boot(bootDir).forEach {
            processes[it.pid()] = it
        }
    }

    private fun stopBotProcess(pid: Pid) {
        val process = processes[pid]
        if (process == null) {
            println("lost: $pid")
        } else {
            stopProcess(process)
        }
    }

    private fun boot(bootDir: Path): Set {
        getBootEntry(bootDir)?.let { bootEntry ->
            bootEntry.apply {
                if (teamMembers?.isNotEmpty() == true) {
                    return bootTeam(bootDir, Team(teamId, bootEntry.name, bootEntry.version, teamMembers))
                }
            }
            bootBot(bootDir)?.let { return setOf(it) }
        }
        return emptySet()
    }

    private fun findBootScriptOrNull(botDir: Path): Path? {
        val scriptPath = findOsScript(botDir)
        if (scriptPath == null) {
            System.err.println("ERROR: No script found within the bot directory: $botDir")
        }
        return scriptPath
    }

    private fun bootTeam(bootDir: Path, team: Team): Set {
        val parentPath = bootDir.parent

        val botProcesses = HashSet()

        team.members.forEach { botName ->
            val botDir = parentPath.resolve(botName)
            findBootScriptOrNull(botDir)?.let {
                bootBot(botDir, team)?.let {
                    botProcesses.add(it)
                }
            }
        }

        teamId++

        return botProcesses
    }

    private fun bootBot(botDir: Path, team: Team? = null): Process? {
        getBootEntry(botDir)?.let { botEntry ->
            findBootScriptOrNull(botDir)?.let { scriptPath ->

                val processBuilder = createProcessBuilder(scriptPath.toString())
                processBuilder.directory(scriptPath.parent.toFile()) // set working directory

                // important to transfer env. variables for bot to the process
                val envMap = processBuilder.environment()
                setEnvVars(envMap, botEntry)
                team?.let {
                    envMap["TEAM_ID"] = team.id.toString()
                    envMap["TEAM_NAME"] = team.name
                    envMap["TEAM_VERSION"] = team.version
                }
                val process = processBuilder.start().also {
                    println("${it.pid()};${botDir.absolutePathString()}")
                    processes[it.pid()] = it
                }
                return process
            }
        }
        return null
    }

    private fun killAllProcesses() {
        processes.values.parallelStream().forEach { stopProcess(it) }
    }

    private fun stopProcess(process: Process) {
        process.apply {
            val pid = pid()
            onExit().thenAccept {
                processes.remove(pid)
                println("stopped $pid")
            }
            descendants().forEach { it.destroyForcibly() }
            destroyForcibly()
        }
    }

    private fun createProcessBuilder(command: String): ProcessBuilder {
        val cmd = command.lowercase()
        return when {
            cmd.endsWith(".bat") -> // handle Batch script
                ProcessBuilder("cmd.exe", "/c \"$command\"")

            cmd.endsWith(".sh") -> // handle Bash Shell script
                ProcessBuilder("bash", "-c", "\"$command\"")

            else -> // handle regular command
                ProcessBuilder(command)
        }
    }

    private fun findOsScript(botDir: Path): Path? = when (OperatingSystemCheck.getOperatingSystemType()) {
        Windows -> findWindowsScript(botDir)
        Mac -> findMacOsScript(botDir)
        else -> findFirstUnixScript(botDir)
    }

    private fun findWindowsScript(botDir: Path): Path? {
        val botName = botDir.fileName.toString()

        var path = botDir.resolve("$botName.bat")
        if (path.exists()) return path

        path = botDir.resolve("$botName.cmd")
        if (path.exists()) return path

        return findFirstUnixScript(botDir)
    }

    private fun findMacOsScript(botDir: Path): Path? {
        val botName = botDir.fileName.toString()

        val path = botDir.resolve("$botName.command")
        if (path.exists()) return path

        return findFirstUnixScript(botDir)
    }

    private fun findFirstUnixScript(botDir: Path): Path? {
        val botName = botDir.fileName.toString()

        val path = botDir.resolve("$botName.sh")
        if (path.exists()) return path

        // Look for any file with no file extension or where the file containing the '#!' characters, i.e. a script

        list(botDir).filter(IsBotFile(botName)).collect(toList()).forEach { filePath ->
            if (filePath.fileName.toString().equals(botName, ignoreCase = true))
                return filePath
            if (readFirstLine(botDir.resolve(filePath)).trim().startsWith("#!"))
                return filePath
        }
        return null // No path found
    }

    companion object {
        private fun readFirstLine(path: Path): String {
            return Files.newInputStream(path).bufferedReader().readLine() ?: ""
        }

        private fun setEnvVars(envMap: MutableMap, bootEntry: BootEntry) {
            System.getProperty("server.url")?.let {
                envMap[Env.SERVER_URL.name] = it
            }
            System.getProperty("server.secret")?.let {
                envMap[Env.SERVER_SECRET.name] = it
            }

            envMap[Env.BOT_BOOTED.name] = "true"

            envMap[Env.BOT_NAME.name] = bootEntry.name
            envMap[Env.BOT_VERSION.name] = bootEntry.version
            envMap[Env.BOT_AUTHORS.name] = bootEntry.authors.joinToString()

            bootEntry.gameTypes?.let {
                envMap[Env.BOT_GAME_TYPES.name] = bootEntry.gameTypes.joinToString()
            }
            bootEntry.description?.let {
                envMap[Env.BOT_DESCRIPTION.name] = it
            }
            bootEntry.homepage?.let {
                envMap[Env.BOT_HOMEPAGE.name] = it
            }
            bootEntry.countryCodes?.let {
                envMap[Env.BOT_COUNTRY_CODES.name] = it.joinToString()
            }
            bootEntry.platform?.let {
                envMap[Env.BOT_PLATFORM.name] = it
            }
            bootEntry.programmingLang?.let {
                envMap[Env.BOT_PROG_LANG.name] = it
            }
            bootEntry.initialPosition?.let {
                envMap[Env.BOT_INITIAL_POS.name] = it
            }
        }
    }
}

internal class IsBotFile(private val botName: String) : Predicate {

    override fun test(path: Path): Boolean {
        val filename = path.fileName.toString().lowercase()
        val botName = botName.lowercase()

        return filename == botName || filename.startsWith("$botName.")
    }
}

internal class Team(val id: TeamId, val name: String, val version: String, val members: List)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy