Compare commits
No commits in common. "e39446cd4743aa4455b2c19dea0e96d6a7dfc378" and "72625833cdd003505f0f680b537c6638472f1658" have entirely different histories.
e39446cd47
...
72625833cd
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.21" />
|
<option name="version" value="1.9.20" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.21"
|
kotlin("jvm") version "1.9.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "org.example"
|
group = "org.example"
|
||||||
@ -18,8 +18,6 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-server-default-headers-jvm:2.3.6")
|
implementation("io.ktor:ktor-server-default-headers-jvm:2.3.6")
|
||||||
implementation("io.ktor:ktor-server-html-builder:2.3.6")
|
implementation("io.ktor:ktor-server-html-builder:2.3.6")
|
||||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-css:1.0.0-pre.650")
|
implementation("org.jetbrains.kotlin-wrappers:kotlin-css:1.0.0-pre.650")
|
||||||
|
|
||||||
implementation("com.jcraft:jsch:0.1.55")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
object Css {
|
object Css {
|
||||||
val cssString = """
|
val cssString = """
|
||||||
.teamView{
|
.teamView{
|
||||||
@ -8,7 +6,6 @@ object Css {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.team{
|
.team{
|
||||||
border-radius: 20px;
|
|
||||||
width: 50%;
|
width: 50%;
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
@ -20,18 +17,14 @@ object Css {
|
|||||||
background-color: #ffbbbb;
|
background-color: #ffbbbb;
|
||||||
}
|
}
|
||||||
.squad{
|
.squad{
|
||||||
|
background-color: #00000020;
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
}
|
}
|
||||||
.squadTable{
|
.squadTable{
|
||||||
background-color: #00000020;
|
|
||||||
border-radius: 0px 0px 5px 5px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
.squadHeader{
|
.squadHeader{
|
||||||
padding-left: 10px;
|
background-color: #00000050
|
||||||
background-color: #00000050;
|
|
||||||
border-radius: 5px 5px 0px 0px;
|
|
||||||
}
|
}
|
||||||
.leader{
|
.leader{
|
||||||
background-color: #CD7F32
|
background-color: #CD7F32
|
||||||
0
src/main/kotlin/LayerInfo.kt
Normal file
0
src/main/kotlin/LayerInfo.kt
Normal file
@ -1,5 +1,3 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object LayerInfoModule : Module() {
|
object LayerInfoModule : Module() {
|
||||||
@ -1,5 +1,3 @@
|
|||||||
package yenon.squadcompanion.logreader
|
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
@ -9,7 +7,7 @@ import java.nio.file.Path
|
|||||||
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
||||||
import kotlin.io.path.name
|
import kotlin.io.path.name
|
||||||
|
|
||||||
class LogReaderTail(path: Path, private val lineCallback: (String) -> Unit) : AutoCloseable {
|
class LogReader(path: Path, private val lineCallback: (String) -> Unit) : AutoCloseable {
|
||||||
private var reader: BufferedInputStream
|
private var reader: BufferedInputStream
|
||||||
private var watchJob: Job
|
private var watchJob: Job
|
||||||
private var readerJob: Job
|
private var readerJob: Job
|
||||||
@ -61,7 +59,7 @@ class LogReaderTail(path: Path, private val lineCallback: (String) -> Unit) : Au
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processByteArray(byteArray: ByteArray) {
|
fun processByteArray(byteArray: ByteArray) {
|
||||||
val lines = String(byteArray).split("\n")
|
val lines = String(byteArray).split("\n")
|
||||||
|
|
||||||
stringBuilder.append(lines[0])
|
stringBuilder.append(lines[0])
|
||||||
124
src/main/kotlin/Main.kt
Normal file
124
src/main/kotlin/Main.kt
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.html.*
|
||||||
|
import io.ktor.server.netty.*
|
||||||
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
val squadTimeFormat = SimpleDateFormat("yyyy.MM.dd-HH.mm.ss:SSS")
|
||||||
|
val logRegex = Regex("""^\[(\d{4}.\d{2}.\d{2}-\d{2}.\d{2}.\d{2}:\d{3})]\[([\d ]{3})](.*)$""")
|
||||||
|
|
||||||
|
val modules = arrayListOf<Module>(LayerInfoModule)
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
LogReader(Paths.get("SquadGame.log")) { logLine ->
|
||||||
|
logRegex.find(logLine)?.let { matchResult ->
|
||||||
|
val time = squadTimeFormat.parse(matchResult.groupValues[1])
|
||||||
|
val id = matchResult.groupValues[2].trim().toShort()
|
||||||
|
val message = matchResult.groupValues[3]
|
||||||
|
|
||||||
|
modules.forEach {
|
||||||
|
if (it.enabled) {
|
||||||
|
it.onLogMessage(time, id, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val connection = RconConnection("195.201.62.238", 21114, "ThisIsJustATestServer")
|
||||||
|
val abstraction = RconAbstraction(connection)
|
||||||
|
GlobalScope.launch {
|
||||||
|
|
||||||
|
//if(connection.reconnect().isFailure){
|
||||||
|
// println("No connection :(")
|
||||||
|
//}
|
||||||
|
//println(connection.sendCommand("ListLayers"))
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedServer(Netty, 8080) {
|
||||||
|
install(StatusPages) {
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
val writer = StringWriter()
|
||||||
|
cause.printStackTrace(PrintWriter(writer))
|
||||||
|
call.respondText(text = "500: $writer", status = HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routing {
|
||||||
|
get("/layers") {
|
||||||
|
call.respondText(
|
||||||
|
contentType = ContentType.Text.Plain,
|
||||||
|
text = "Count: " + LayerInfoModule.layerList.size + "\n\n" +
|
||||||
|
LayerInfoModule.layerList.joinToString("\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get("/rotation") {
|
||||||
|
call.respondText(
|
||||||
|
contentType = ContentType.Text.Plain,
|
||||||
|
text = "Count: " + LayerInfoModule.rotationList.size + "\n\n" +
|
||||||
|
LayerInfoModule.rotationList.joinToString("\n")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get("/currentPlayers") {
|
||||||
|
call.respondText(
|
||||||
|
contentType = ContentType.Text.Plain,
|
||||||
|
text = abstraction.getCurrentPlayers().toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
get("/overview") {
|
||||||
|
val players = abstraction.getCurrentPlayers()
|
||||||
|
val squadInfo = abstraction.getSquadList()
|
||||||
|
|
||||||
|
call.respondHtmlTemplate(OverviewTemplate(players, squadInfo)) {}
|
||||||
|
}
|
||||||
|
get("/overviewDemo") {
|
||||||
|
//val players = abstraction.getCurrentPlayers()
|
||||||
|
//val squadInfo = abstraction.getSquadList()
|
||||||
|
val players = ListPlayersOutput(
|
||||||
|
arrayListOf(
|
||||||
|
Player(1, 86868686868686868L, "newSlMan", 1, 1, true, "SL"),
|
||||||
|
Player(2, 31337313373133711L, "IStayed(TM)", 2, 1, true, "SL"),
|
||||||
|
Player(1, 1L, "Man1", 1, 1, false, "RM"),
|
||||||
|
Player(1, 2L, "Man2", 1, 1, false, "LAT"),
|
||||||
|
Player(1, 3L, "Man3", 1, 1, false, "HAT"),
|
||||||
|
Player(1, 4L, "Man4", 1, 1, false, "MED"),
|
||||||
|
Player(1, 5L, "Man5", 1, 1, false, "GR"),
|
||||||
|
Player(1, 6L, "OtherSL1", 2, 2, true, "SL"),
|
||||||
|
Player(1, 7L, "OtherMan2", 2, 2, false, "MED"),
|
||||||
|
Player(1, 8L, "OtherMan3", 2, 0, false, "RM"),
|
||||||
|
Player(1, 9L, "OtherMan4", 2, 0, false, "RM"),
|
||||||
|
Player(1, 10L, "OtherMan5", 2, 0, false, "RM"),
|
||||||
|
), arrayListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
val squadInfo = Pair(
|
||||||
|
RconAbstraction.SquadListFaction(
|
||||||
|
"1st Battalion, 1st Marines", hashMapOf(
|
||||||
|
1 to RconAbstraction.SquadListSquad("HERPS", 5, true, "leaver9000", 69696969696969696L)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RconAbstraction.SquadListFaction(
|
||||||
|
"Insurgent Rebel Federation", hashMapOf(
|
||||||
|
1 to RconAbstraction.SquadListSquad("DERPS", 1, true, "IStayed(TM)", 31337313373133711L),
|
||||||
|
2 to RconAbstraction.SquadListSquad("NOOBS WELCOME", 2, false, "OtherSL1", 6L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
call.respondHtmlTemplate(OverviewTemplate(players, squadInfo)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get("/css") {
|
||||||
|
call.respondText(contentType = ContentType.Text.CSS, text = Css.cssString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(true)
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class Module {
|
abstract class Module {
|
||||||
@ -1,13 +1,11 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
import io.ktor.server.html.*
|
import io.ktor.server.html.*
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
|
||||||
class OverviewTemplate(
|
class OverviewTemplate(
|
||||||
private val players: RconAbstraction.ListPlayersOutput,
|
private val players: ListPlayersOutput,
|
||||||
private val squadInfo: Pair<RconAbstraction.SquadListFaction, RconAbstraction.SquadListFaction>
|
private val squadInfo: Pair<RconAbstraction.SquadListFaction, RconAbstraction.SquadListFaction>
|
||||||
) : Template<HTML> {
|
) : Template<HTML> {
|
||||||
private fun TR.makePlayer(player: RconAbstraction.Player) {
|
private fun TR.makePlayer(player: Player) {
|
||||||
td { +player.role }
|
td { +player.role }
|
||||||
td { +player.name }
|
td { +player.name }
|
||||||
td { +player.steamId.toString() }
|
td { +player.steamId.toString() }
|
||||||
@ -25,28 +23,24 @@ class OverviewTemplate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun FlowContent.makeSquad(
|
private fun FlowContent.makeSquad(
|
||||||
squadMembers: Map.Entry<Int, List<RconAbstraction.Player>>,
|
squad: Map.Entry<Int, List<Player>>,
|
||||||
squad: RconAbstraction.SquadListSquad?
|
squadListSquad: RconAbstraction.SquadListSquad
|
||||||
) {
|
) {
|
||||||
val leader = squadMembers.value.find { it.leader }
|
val leader = squad.value.find { it.leader }
|
||||||
div("squad") {
|
div("squad") {
|
||||||
div("squadHeader") {
|
div("squadHeader") {
|
||||||
if (squad == null) {
|
if (squad.key != 0) {
|
||||||
+"Squad info not available, refresh page."
|
+squadListSquad.name
|
||||||
} else {
|
|
||||||
if (squadMembers.key != 0) {
|
|
||||||
+squad.name
|
|
||||||
} else {
|
} else {
|
||||||
+"Unassigned"
|
+"Unassigned"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (squad.locked) {
|
if (squadListSquad.locked) {
|
||||||
+" \uD83D\uDD12"
|
+" \uD83D\uDD12"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (squad.creatorSteamId != (leader?.steamId ?: 0) && squad.creatorSteamId != 0L) {
|
if (squadListSquad.creatorSteamId != (leader?.steamId ?: 0) && squadListSquad.creatorSteamId != 0L) {
|
||||||
+" (originally created by ${squad.creatorName} ${squad.creatorSteamId})"
|
+" (originally created by ${squadListSquad.creatorName} ${squadListSquad.creatorSteamId})"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +58,7 @@ class OverviewTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
squadMembers.value.forEach {
|
squad.value.forEach {
|
||||||
if (!it.leader) {
|
if (!it.leader) {
|
||||||
tr {
|
tr {
|
||||||
makePlayer(it)
|
makePlayer(it)
|
||||||
@ -75,10 +69,7 @@ class OverviewTemplate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FlowContent.makeSide(
|
private fun FlowContent.makeSide(team: Map<Int, List<Player>>, squadListFaction: RconAbstraction.SquadListFaction) {
|
||||||
team: Map<Int, List<RconAbstraction.Player>>,
|
|
||||||
squadListFaction: RconAbstraction.SquadListFaction
|
|
||||||
) {
|
|
||||||
div("teamHeader") {
|
div("teamHeader") {
|
||||||
+squadListFaction.name
|
+squadListFaction.name
|
||||||
}
|
}
|
||||||
@ -88,7 +79,7 @@ class OverviewTemplate(
|
|||||||
}
|
}
|
||||||
team.forEach {
|
team.forEach {
|
||||||
if (it.key != 0) {
|
if (it.key != 0) {
|
||||||
makeSquad(it, squadListFaction.squads[it.key])
|
makeSquad(it, squadListFaction.squads[it.key]!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
team.filter { it.key == 0 }.forEach {
|
team.filter { it.key == 0 }.forEach {
|
||||||
@ -1,16 +1,4 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
class RconAbstraction(private val connection: RconConnection) {
|
|
||||||
companion object {
|
|
||||||
val activePlayerInListRegex = Regex(
|
|
||||||
"""ID: (\d+) \| SteamID: (\d{17}) \| Name: (.*) \| Team ID: ([12]) \| Squad ID: (N/A|\d+) \| Is Leader: (False|True) \| Role: ([a-zA-Z0-9_]+)${'$'}""",
|
|
||||||
RegexOption.MULTILINE
|
|
||||||
)
|
|
||||||
val inactivePlayerInListRegex = Regex(
|
|
||||||
"""ID: (\d+) \| SteamID: (\d{17}) \| Since Disconnect: (\d{2}m\.\d{2})s \| Name: (.*)${'$'}""",
|
|
||||||
RegexOption.MULTILINE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ListPlayersOutput(val players: ArrayList<Player>, val disconnected: ArrayList<DisconnectedPlayer>)
|
data class ListPlayersOutput(val players: ArrayList<Player>, val disconnected: ArrayList<DisconnectedPlayer>)
|
||||||
data class Player(
|
data class Player(
|
||||||
@ -25,6 +13,17 @@ class RconAbstraction(private val connection: RconConnection) {
|
|||||||
|
|
||||||
data class DisconnectedPlayer(val playerId: Int, val steamId: Long, val time: String, val name: String)
|
data class DisconnectedPlayer(val playerId: Int, val steamId: Long, val time: String, val name: String)
|
||||||
|
|
||||||
|
val activePlayerInListRegex = Regex(
|
||||||
|
"""ID: (\d+) \| SteamID: (\d{17}) \| Name: (.*) \| Team ID: ([12]) \| Squad ID: (N/A|\d+) \| Is Leader: (False|True) \| Role: ([a-zA-Z0-9_]+)${'$'}""",
|
||||||
|
RegexOption.MULTILINE
|
||||||
|
)
|
||||||
|
val inactivePlayerInListRegex = Regex(
|
||||||
|
"""ID: (\d+) \| SteamID: (\d{17}) \| Since Disconnect: (\d{2}m\.\d{2})s \| Name: (.*)${'$'}""",
|
||||||
|
RegexOption.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
class RconAbstraction(private val connection: RconConnection) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Format for "ListPlayers"
|
Format for "ListPlayers"
|
||||||
----- Active Players -----
|
----- Active Players -----
|
||||||
@ -80,7 +79,7 @@ class RconAbstraction(private val connection: RconConnection) {
|
|||||||
|
|
||||||
val teamRegex = Regex("""^Team ID: ([12]) \(([^)]+)\)$""")
|
val teamRegex = Regex("""^Team ID: ([12]) \(([^)]+)\)$""")
|
||||||
val squadRegex =
|
val squadRegex =
|
||||||
Regex("""^ID: (\d+) \| Name: (.*) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.*) \| Creator Steam ID: (\d{17})$""")
|
Regex("""^ID: (\d+) \| Name: (.*) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.*) \| Creator Steam ID: (\d{17})${'$'}""")
|
||||||
|
|
||||||
lateinit var faction1: SquadListFaction
|
lateinit var faction1: SquadListFaction
|
||||||
lateinit var faction2: SquadListFaction
|
lateinit var faction2: SquadListFaction
|
||||||
182
src/main/kotlin/RconConnection.kt
Normal file
182
src/main/kotlin/RconConnection.kt
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import io.ktor.utils.io.nio.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.nio.channels.SocketChannel
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.text.toByteArray
|
||||||
|
|
||||||
|
const val SERVERDATA_AUTH = 3
|
||||||
|
const val SERVERDATA_AUTH_RESPONSE = 2
|
||||||
|
const val SERVERDATA_EXECCOMMAND = 2
|
||||||
|
const val SERVERDATA_RESPONSE_VALUE = 0
|
||||||
|
|
||||||
|
const val FAILED_AUTH_ID = -1
|
||||||
|
const val ACCEPTED_AUTH_ID = 1
|
||||||
|
const val SQUAD_CONSOLE_ID = 0
|
||||||
|
|
||||||
|
val rconCharset = Charsets.UTF_8
|
||||||
|
|
||||||
|
data class RconPacket(val id: Int, val type: Int, val data: ByteArray)
|
||||||
|
|
||||||
|
class RconConnection(private val host: String, private val port: Int, password: String) : AutoCloseable {
|
||||||
|
|
||||||
|
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private val authPacket =
|
||||||
|
createPacketByteBuffer(RconPacket(ACCEPTED_AUTH_ID, SERVERDATA_AUTH, password.toByteArray(rconCharset)))
|
||||||
|
private var currentId = ACCEPTED_AUTH_ID + 1
|
||||||
|
private var socketChannel: SocketChannel = SocketChannel.open()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(5000)
|
||||||
|
sendCommand("ListPlayers")
|
||||||
|
//println("Keepalive: "+sendCommand("ListPlayers"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun reconnect(): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
|
socketChannel.close()
|
||||||
|
socketChannel = SocketChannel.open().apply {
|
||||||
|
connect(InetSocketAddress(host, port))
|
||||||
|
finishConnect()
|
||||||
|
println("Socket is open.")
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun authenticate(): Result<Boolean> {
|
||||||
|
println("Sending auth packet")
|
||||||
|
socketChannel.write(authPacket)
|
||||||
|
println("Auth sent")
|
||||||
|
readPacket()
|
||||||
|
val packet = readPacket()
|
||||||
|
println("Response: $packet")
|
||||||
|
return if (packet.type == SERVERDATA_AUTH_RESPONSE) {
|
||||||
|
println(packet.id == ACCEPTED_AUTH_ID)
|
||||||
|
Result.success(packet.id == ACCEPTED_AUTH_ID)
|
||||||
|
} else {
|
||||||
|
Result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendCommand(command: String): Result<String> = withContext(Dispatchers.IO) {
|
||||||
|
mutex.withLock {
|
||||||
|
if (!socketChannel.isConnected) {
|
||||||
|
println("reconnect needed.")
|
||||||
|
val reconnectResult = reconnect()
|
||||||
|
if (reconnectResult.isFailure) {
|
||||||
|
return@withContext Result.failure(reconnectResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
reconnectResult.getOrNull()?.let {
|
||||||
|
if (!it) {
|
||||||
|
return@withContext Result.failure(Exception("Wrong password."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentId++
|
||||||
|
val commandPacket =
|
||||||
|
createPacketByteBuffer(RconPacket(currentId, SERVERDATA_EXECCOMMAND, command.toByteArray(rconCharset)))
|
||||||
|
socketChannel.write(commandPacket)
|
||||||
|
|
||||||
|
var responsePacket = readPacket()
|
||||||
|
//while (responsePacket.id<currentId){
|
||||||
|
// responsePacket = readPacket()
|
||||||
|
//}
|
||||||
|
if (responsePacket.id != currentId || responsePacket.type != SERVERDATA_RESPONSE_VALUE) {
|
||||||
|
return@withContext Result.failure(
|
||||||
|
Exception(
|
||||||
|
"Out of order packet. Expected:$currentId, Got:${responsePacket.id}, Data:${
|
||||||
|
String(
|
||||||
|
responsePacket.data,
|
||||||
|
rconCharset
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsePacket.data.size < 4084) {
|
||||||
|
//Short packet, no additional handling needed.
|
||||||
|
return@withContext Result.success(String(responsePacket.data, rconCharset))
|
||||||
|
}
|
||||||
|
|
||||||
|
//Long packet, we need to make sure we read all of it and receive an additional empty packet from Squad.
|
||||||
|
|
||||||
|
val packetByteList = LinkedList<ByteArray>()
|
||||||
|
do {
|
||||||
|
packetByteList.add(responsePacket.data)
|
||||||
|
responsePacket = readPacket()
|
||||||
|
} while (responsePacket.data.isEmpty())
|
||||||
|
|
||||||
|
if (responsePacket.id != currentId) {
|
||||||
|
return@withContext Result.failure(Exception("Out of order packet."))
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalBytes = packetByteList.sumOf { it.size }
|
||||||
|
val combinedBytes = ByteArray(totalBytes)
|
||||||
|
var start = 0
|
||||||
|
|
||||||
|
packetByteList.forEach {
|
||||||
|
it.copyInto(combinedBytes, start)
|
||||||
|
start += it.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext Result.success(String(combinedBytes, rconCharset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readPacket(): RconPacket {
|
||||||
|
var packet = readRawPacket()
|
||||||
|
while (packet.id == 0 && packet.type == 1) {
|
||||||
|
println("Console ${packet.data.size}: " + String(packet.data))
|
||||||
|
packet = readRawPacket()
|
||||||
|
}
|
||||||
|
return packet
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readRawPacket(): RconPacket {
|
||||||
|
print("Reading new packet")
|
||||||
|
val size = socketChannel.readPacketExact(4L).readIntLittleEndian()
|
||||||
|
println(", size: $size")
|
||||||
|
val packet = socketChannel.readPacketExact(size.toLong())
|
||||||
|
|
||||||
|
val id = packet.readIntLittleEndian()
|
||||||
|
val type = packet.readIntLittleEndian()
|
||||||
|
val data = packet.readBytes(size - 10)
|
||||||
|
println("Read packet with id: $id, type: $type")
|
||||||
|
|
||||||
|
return RconPacket(id, type, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPacketByteBuffer(packet: RconPacket): ByteBuffer {
|
||||||
|
val length = 4 + 4 + 4 + packet.data.size + 2
|
||||||
|
val buffer = ByteBuffer.allocate(length)
|
||||||
|
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
buffer.putInt(length - 4)
|
||||||
|
buffer.putInt(packet.id)
|
||||||
|
buffer.putInt(packet.type)
|
||||||
|
|
||||||
|
buffer.put(packet.data)
|
||||||
|
|
||||||
|
//rcon needs 0 terminated string with additional 0 as end of packet
|
||||||
|
buffer.put(0)
|
||||||
|
buffer.put(0)
|
||||||
|
|
||||||
|
return buffer.flip() as ByteBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
socketChannel.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,145 +0,0 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.application.*
|
|
||||||
import io.ktor.server.engine.*
|
|
||||||
import io.ktor.server.html.*
|
|
||||||
import io.ktor.server.netty.*
|
|
||||||
import io.ktor.server.plugins.statuspages.*
|
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import yenon.squadcompanion.logreader.LogReaderTail
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
|
|
||||||
object Main {
|
|
||||||
private val squadTimeFormat = SimpleDateFormat("yyyy.MM.dd-HH.mm.ss:SSS")
|
|
||||||
private val logRegex = Regex("""^\[(\d{4}.\d{2}.\d{2}-\d{2}.\d{2}.\d{2}:\d{3})]\[([\d ]{3})](.*)$""")
|
|
||||||
|
|
||||||
private val modules = arrayListOf<Module>(LayerInfoModule)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(vararg args: String) {
|
|
||||||
LogReaderTail(Paths.get("SquadGame.log")) { logLine ->
|
|
||||||
logRegex.find(logLine)?.let { matchResult ->
|
|
||||||
val time = squadTimeFormat.parse(matchResult.groupValues[1])
|
|
||||||
val id = matchResult.groupValues[2].trim().toShort()
|
|
||||||
val message = matchResult.groupValues[3]
|
|
||||||
|
|
||||||
modules.forEach {
|
|
||||||
if (it.enabled) {
|
|
||||||
it.onLogMessage(time, id, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val abstraction = runBlocking {
|
|
||||||
val connection = RconConnection("195.201.62.238", 21114, "ThisIsJustATestServer")
|
|
||||||
connection.connect()
|
|
||||||
RconAbstraction(connection)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
embeddedServer(Netty, 8080) {
|
|
||||||
install(StatusPages) {
|
|
||||||
exception<Throwable> { call, cause ->
|
|
||||||
val writer = StringWriter()
|
|
||||||
cause.printStackTrace(PrintWriter(writer))
|
|
||||||
call.respondText(text = "500: $writer", status = HttpStatusCode.InternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routing {
|
|
||||||
get("/layers") {
|
|
||||||
call.respondText(
|
|
||||||
contentType = ContentType.Text.Plain,
|
|
||||||
text = "Count: " + LayerInfoModule.layerList.size + "\n\n" +
|
|
||||||
LayerInfoModule.layerList.joinToString("\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
get("/rotation") {
|
|
||||||
call.respondText(
|
|
||||||
contentType = ContentType.Text.Plain,
|
|
||||||
text = "Count: " + LayerInfoModule.rotationList.size + "\n\n" +
|
|
||||||
LayerInfoModule.rotationList.joinToString("\n")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
get("/currentPlayers") {
|
|
||||||
call.respondText(
|
|
||||||
contentType = ContentType.Text.Plain,
|
|
||||||
text = abstraction.getCurrentPlayers().toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
get("/overview") {
|
|
||||||
val players = abstraction.getCurrentPlayers()
|
|
||||||
val squadInfo = abstraction.getSquadList()
|
|
||||||
|
|
||||||
call.respondHtmlTemplate(OverviewTemplate(players, squadInfo)) {}
|
|
||||||
}
|
|
||||||
get("/overviewDemo") {
|
|
||||||
val players = RconAbstraction.ListPlayersOutput(
|
|
||||||
arrayListOf(
|
|
||||||
RconAbstraction.Player(1, 86868686868686868L, "newSlMan", 1, 1, true, "SL"),
|
|
||||||
RconAbstraction.Player(2, 31337313373133711L, "IStayed(TM)", 2, 1, true, "SL"),
|
|
||||||
RconAbstraction.Player(1, 1L, "Man1", 1, 1, false, "RM"),
|
|
||||||
RconAbstraction.Player(1, 2L, "Man2", 1, 1, false, "LAT"),
|
|
||||||
RconAbstraction.Player(1, 3L, "Man3", 1, 1, false, "HAT"),
|
|
||||||
RconAbstraction.Player(1, 4L, "Man4", 1, 1, false, "MED"),
|
|
||||||
RconAbstraction.Player(1, 5L, "Man5", 1, 1, false, "GR"),
|
|
||||||
RconAbstraction.Player(1, 6L, "OtherSL1", 2, 2, true, "SL"),
|
|
||||||
RconAbstraction.Player(1, 7L, "OtherMan2", 2, 2, false, "MED"),
|
|
||||||
RconAbstraction.Player(1, 8L, "OtherMan3", 2, 0, false, "RM"),
|
|
||||||
RconAbstraction.Player(1, 9L, "OtherMan4", 2, 0, false, "RM"),
|
|
||||||
RconAbstraction.Player(1, 10L, "OtherMan5", 2, 0, false, "RM"),
|
|
||||||
), arrayListOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
val squadInfo = Pair(
|
|
||||||
RconAbstraction.SquadListFaction(
|
|
||||||
"1st Battalion, 1st Marines", hashMapOf(
|
|
||||||
1 to RconAbstraction.SquadListSquad("HERPS", 5, true, "leaver9000", 69696969696969696L)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
RconAbstraction.SquadListFaction(
|
|
||||||
"Insurgent Rebel Federation", hashMapOf(
|
|
||||||
1 to RconAbstraction.SquadListSquad(
|
|
||||||
"DERPS",
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
"IStayed(TM)",
|
|
||||||
31337313373133711L
|
|
||||||
),
|
|
||||||
2 to RconAbstraction.SquadListSquad("NOOBS WELCOME", 2, false, "OtherSL1", 6L)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
call.respondHtmlTemplate(OverviewTemplate(players, squadInfo)) {}
|
|
||||||
}
|
|
||||||
get("/css") {
|
|
||||||
call.respondText(contentType = ContentType.Text.CSS, text = Css.cssString)
|
|
||||||
}
|
|
||||||
post("/kick") {
|
|
||||||
val playerId = call.request.queryParameters["id"]!!.toLong()
|
|
||||||
val reason = call.receiveText()
|
|
||||||
abstraction.kickPlayer(playerId, reason)
|
|
||||||
}
|
|
||||||
post("/ban") {
|
|
||||||
val playerId = call.request.queryParameters["id"]!!.toLong()
|
|
||||||
val time = call.request.queryParameters["time"]!!
|
|
||||||
val reason = call.receiveText()
|
|
||||||
abstraction.banPlayer(playerId, time, reason)
|
|
||||||
}
|
|
||||||
post("/message") {
|
|
||||||
val playerId = call.request.queryParameters["id"]!!.toLong()
|
|
||||||
val message = call.receiveText()
|
|
||||||
abstraction.messagePlayer(playerId, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
package yenon.squadcompanion
|
|
||||||
|
|
||||||
import io.ktor.utils.io.core.*
|
|
||||||
import io.ktor.utils.io.errors.*
|
|
||||||
import io.ktor.utils.io.nio.*
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.nio.channels.SocketChannel
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.text.toByteArray
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
class RconConnection(private val host: String, private val port: Int, password: String) : AutoCloseable {
|
|
||||||
companion object {
|
|
||||||
const val TYPE_AUTH = 3
|
|
||||||
const val TYPE_AUTH_RESPONSE = 2
|
|
||||||
const val TYPE_EXECCOMMAND = 2
|
|
||||||
const val TYPE_RESPONSE_VALUE = 0
|
|
||||||
|
|
||||||
const val ID_AUTH_ACCEPTED = 1
|
|
||||||
const val ID_AUTH_REJECTED = -1
|
|
||||||
const val ID_SQUAD_CONSOLE = 0
|
|
||||||
|
|
||||||
val rconCharset = Charsets.UTF_8
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RconPacket(val id: Int, val type: Int, val data: ByteArray) {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as RconPacket
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
|
||||||
if (type != other.type) return false
|
|
||||||
if (!data.contentEquals(other.data)) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id
|
|
||||||
result = 31 * result + type
|
|
||||||
result = 31 * result + data.contentHashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
private val authPacket =
|
|
||||||
createPacketByteBuffer(RconPacket(ID_AUTH_ACCEPTED, TYPE_AUTH, password.toByteArray(rconCharset)))
|
|
||||||
private var currentId = ID_AUTH_ACCEPTED + 1
|
|
||||||
private var socketChannel: SocketChannel = SocketChannel.open()
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
private val sameIdPacketList = LinkedList<RconPacket>()
|
|
||||||
private val packetsFlow = MutableSharedFlow<RconPacket>(extraBufferCapacity = 8)
|
|
||||||
|
|
||||||
suspend fun connect(): ConnectionStatus {
|
|
||||||
val status = reconnect()
|
|
||||||
if (status != ConnectionStatus.CONNECTED) {
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
while (isActive) {
|
|
||||||
delay(5000)
|
|
||||||
sendCommand("ListPlayers")
|
|
||||||
println("Keepalive: " + sendCommand("ListPlayers"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
coroutineScope.launch {
|
|
||||||
while (isActive) {
|
|
||||||
if (!socketChannel.isConnected) {
|
|
||||||
reconnect()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
var packet = readRawPacket()
|
|
||||||
val firstId = packet.id
|
|
||||||
sameIdPacketList.add(packet)
|
|
||||||
while (packet.data.size == 4084) {
|
|
||||||
packet = readRawPacket()
|
|
||||||
if (packet.id != firstId) {
|
|
||||||
println("Out of order packet during assembly.")
|
|
||||||
println("Data: $packet")
|
|
||||||
println("Packet has been discarded.")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sameIdPacketList.add(packet)
|
|
||||||
}
|
|
||||||
if (sameIdPacketList.size == 1) {
|
|
||||||
packetsFlow.emit(sameIdPacketList[0])
|
|
||||||
} else {
|
|
||||||
val totalByteCount = sameIdPacketList.sumOf { it.data.size }
|
|
||||||
val combinedByteArray = ByteArray(totalByteCount)
|
|
||||||
var start = 0
|
|
||||||
|
|
||||||
sameIdPacketList.forEach {
|
|
||||||
it.data.copyInto(combinedByteArray, start)
|
|
||||||
start += it.data.size
|
|
||||||
}
|
|
||||||
val initialPacket = sameIdPacketList.first
|
|
||||||
val combinedPacket = RconPacket(initialPacket.id, initialPacket.type, combinedByteArray)
|
|
||||||
packetsFlow.emit(combinedPacket)
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
ex.printStackTrace()
|
|
||||||
}
|
|
||||||
sameIdPacketList.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ConnectionStatus.CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ConnectionStatus {
|
|
||||||
CONNECTED, WRONG_PASSWORD, TIMEOUT
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reconnect(): ConnectionStatus = withContext(Dispatchers.IO) {
|
|
||||||
socketChannel.close()
|
|
||||||
try {
|
|
||||||
socketChannel = SocketChannel.open().apply {
|
|
||||||
connect(InetSocketAddress(host, port))
|
|
||||||
finishConnect()
|
|
||||||
println("Socket is open.")
|
|
||||||
}
|
|
||||||
} catch (ex: IOException) {
|
|
||||||
return@withContext ConnectionStatus.TIMEOUT
|
|
||||||
}
|
|
||||||
|
|
||||||
return@withContext if (authenticate()) {
|
|
||||||
ConnectionStatus.CONNECTED
|
|
||||||
} else {
|
|
||||||
ConnectionStatus.WRONG_PASSWORD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun authenticate(): Boolean {
|
|
||||||
println("Sending auth packet")
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
socketChannel.write(authPacket)
|
|
||||||
println("Auth sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
println("Waiting for auth response")
|
|
||||||
//Squad will send 2 packets, we will treat them as random order, even though that should never happen...
|
|
||||||
val authResponse = arrayOf(
|
|
||||||
readRawPacket(), readRawPacket()
|
|
||||||
).firstOrNull { it.type == TYPE_AUTH_RESPONSE }
|
|
||||||
|
|
||||||
return (authResponse?.id ?: ID_AUTH_REJECTED) == ID_AUTH_ACCEPTED
|
|
||||||
}
|
|
||||||
|
|
||||||
class SquadBullshitException : Exception("No empty commands in squad rcon, a faulty response will clog the stream.")
|
|
||||||
|
|
||||||
suspend fun sendCommand(command: String): Result<String> = withContext(Dispatchers.IO) {
|
|
||||||
if (command.isEmpty()) {
|
|
||||||
throw SquadBullshitException()
|
|
||||||
}
|
|
||||||
|
|
||||||
mutex.withLock {
|
|
||||||
val incomingPackets = packetsFlow.asSharedFlow().timeout(5000.milliseconds).catch {}
|
|
||||||
currentId++
|
|
||||||
val commandPacket =
|
|
||||||
createPacketByteBuffer(RconPacket(currentId, TYPE_EXECCOMMAND, command.toByteArray(rconCharset)))
|
|
||||||
socketChannel.write(commandPacket)
|
|
||||||
|
|
||||||
val packet = incomingPackets.firstOrNull { it.id == currentId }
|
|
||||||
|
|
||||||
packet?.let {
|
|
||||||
return@withContext Result.success(String(it.data, rconCharset))
|
|
||||||
}
|
|
||||||
|
|
||||||
return@withContext Result.failure(Exception("No Response"))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readRawPacket(): RconPacket {
|
|
||||||
print("Reading new packet")
|
|
||||||
val size = socketChannel.readPacketExact(4L).readIntLittleEndian()
|
|
||||||
println(", size: $size")
|
|
||||||
val packet = socketChannel.readPacketExact(size.toLong())
|
|
||||||
|
|
||||||
val id = packet.readIntLittleEndian()
|
|
||||||
val type = packet.readIntLittleEndian()
|
|
||||||
val data = packet.readBytes(size - 10)
|
|
||||||
println("Read packet with id: $id, type: $type")
|
|
||||||
|
|
||||||
return RconPacket(id, type, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPacketByteBuffer(packet: RconPacket): ByteBuffer {
|
|
||||||
val length = 4 + 4 + 4 + packet.data.size + 2
|
|
||||||
val buffer = ByteBuffer.allocate(length)
|
|
||||||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
|
||||||
|
|
||||||
buffer.putInt(length - 4)
|
|
||||||
buffer.putInt(packet.id)
|
|
||||||
buffer.putInt(packet.type)
|
|
||||||
|
|
||||||
buffer.put(packet.data)
|
|
||||||
|
|
||||||
//rcon needs 0 terminated string with additional 0 as end of packet
|
|
||||||
buffer.put(0)
|
|
||||||
buffer.put(0)
|
|
||||||
|
|
||||||
return buffer.flip() as ByteBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
socketChannel.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package yenon.squadcompanion.logreader
|
|
||||||
|
|
||||||
import com.jcraft.jsch.ChannelSftp
|
|
||||||
import com.jcraft.jsch.JSch
|
|
||||||
import com.jcraft.jsch.SftpProgressMonitor
|
|
||||||
import java.io.PipedInputStream
|
|
||||||
import java.io.PipedOutputStream
|
|
||||||
|
|
||||||
class LogReaderSftp(val host: String, val port: Int, val file: String, val user: String, val password: String) {
|
|
||||||
private val pipedInput = PipedInputStream()
|
|
||||||
private val pipedOutput = PipedOutputStream(pipedInput)
|
|
||||||
|
|
||||||
private var lastSize = 0L
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val ignoreProgressMonitor = object : SftpProgressMonitor {
|
|
||||||
override fun init(op: Int, src: String?, dest: String?, max: Long) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun count(count: Long): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun end() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val jsch = JSch()
|
|
||||||
val session = jsch.getSession(user, host, port)
|
|
||||||
session.setPassword(password)
|
|
||||||
session.connect()
|
|
||||||
val sftpChannel = session.openChannel("sftp") as ChannelSftp
|
|
||||||
sftpChannel.connect()
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val newSize = sftpChannel.lstat(file).size
|
|
||||||
if (newSize < lastSize) {
|
|
||||||
println("log rotated, resetting")
|
|
||||||
lastSize = 0L
|
|
||||||
}
|
|
||||||
sftpChannel.get(file, pipedOutput, ignoreProgressMonitor, ChannelSftp.RESUME, lastSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import yenon.squadcompanion.logreader.LogReaderTail
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -31,7 +30,7 @@ class WatchTest {
|
|||||||
val targetBackup = Paths.get("testReplace.bak")
|
val targetBackup = Paths.get("testReplace.bak")
|
||||||
|
|
||||||
var index = 0
|
var index = 0
|
||||||
val reader = LogReaderTail(target) {
|
val reader = LogReader(target) {
|
||||||
println(it)
|
println(it)
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user