diff --git a/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt b/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt index 9457e4c..32e09d5 100644 --- a/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt +++ b/android/app/src/main/java/com/masterdns/vpn/util/VpnManager.kt @@ -18,6 +18,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.util.ArrayDeque /** * Singleton bridge between Kotlin UI and Go core. @@ -83,6 +84,68 @@ object VpnManager { private val logBuffer = ArrayDeque(MAX_LOG_LINES) private var logBufferVersion = 0L + private val INDEXED_PROGRESS_REGEX = Regex( + "(?:scan|scanning|resolver|resolvers|mtu|accepted|rejected).{0,40}?(\\d+)\\s*/\\s*(\\d+)", + setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL) + ) + private val TOTAL_CANDIDATES_REGEX = Regex( + "(?:valid\\s+resolvers|resolvers\\s+for\\s+scan|scan\\s+pool|resolver\\s+pool|total\\s+resolvers).{0,20}?(\\d+)", + RegexOption.IGNORE_CASE + ) + private val SCAN_TOTALS_REGEX = Regex( + "via\\s+([^\\s|]+)\\s*\\|.*totals:\\s*valid=(\\d+),\\s*rejected=(\\d+)", + RegexOption.IGNORE_CASE + ) + private val ACTIVE_RESOLVERS_REGEX = Regex( + "Active Resolvers\\s*[:=]\\s*[^\\d-]*(\\d+)", + RegexOption.IGNORE_CASE + ) + private val TOTAL_ACTIVE_REGEX = Regex( + "total\\s+active\\s*[:=]\\s*[^\\d-]*(\\d+)", + RegexOption.IGNORE_CASE + ) + private val REMAINING_REGEX = Regex( + "remaining\\s*[:=]\\s*[^\\d-]*(\\d+)", + RegexOption.IGNORE_CASE + ) + private val SYNCED_MTU_REGEX = Regex( + "Selected Synced Upload MTU:\\s*(\\d+)\\s*\\|\\s*Selected Synced Download MTU:\\s*(\\d+)", + RegexOption.IGNORE_CASE + ) + private val TIMESTAMP_CANDIDATES = listOf( + TimestampCandidate( + Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)(.*)$"), + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "yyyy-MM-dd HH:mm:ss.SSS" + ), + TimestampCandidate( + Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)(.*)$"), + "yyyy-MM-dd'T'HH:mm:ss'Z'", + "yyyy-MM-dd HH:mm:ss" + ), + TimestampCandidate( + Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+UTC(.*)$"), + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss" + ), + TimestampCandidate( + Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s(.*)$"), + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss" + ), + TimestampCandidate( + Regex("^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})(.*)$"), + "yyyy/MM/dd HH:mm:ss", + "yyyy/MM/dd HH:mm:ss" + ) + ) + + private data class TimestampCandidate( + val regex: Regex, + val inputPattern: String, + val outputPattern: String + ) + fun updateState(newState: VpnState) { _state.value = newState if (newState == VpnState.CONNECTED) { @@ -245,36 +308,24 @@ object VpnManager { } private fun parseScanLine(line: String) { - val indexedProgressMatch = Regex( - "(?:scan|scanning|resolver|resolvers|mtu|accepted|rejected).{0,40}?(\\d+)\\s*/\\s*(\\d+)", - setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL) - ).find(line) - if (indexedProgressMatch != null) { - val total = indexedProgressMatch.groupValues[2].toIntOrNull() + INDEXED_PROGRESS_REGEX.find(line)?.let { match -> + val total = match.groupValues[2].toIntOrNull() if (total != null && total > 0) { _scanStatus.value = _scanStatus.value.copy(scanTotalFromCore = total) } } - val totalCandidatesMatch = Regex( - "(?:valid\\s+resolvers|resolvers\\s+for\\s+scan|scan\\s+pool|resolver\\s+pool|total\\s+resolvers).{0,20}?(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (totalCandidatesMatch != null) { - val total = totalCandidatesMatch.groupValues[1].toIntOrNull() + TOTAL_CANDIDATES_REGEX.find(line)?.let { match -> + val total = match.groupValues[1].toIntOrNull() if (total != null && total > 0) { _scanStatus.value = _scanStatus.value.copy(scanTotalFromCore = total) } } - val scanMatch = Regex( - "via\\s+([^\\s|]+)\\s*\\|.*totals:\\s*valid=(\\d+),\\s*rejected=(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (scanMatch != null) { - val resolver = scanMatch.groupValues[1] - val valid = scanMatch.groupValues[2].toIntOrNull() ?: _scanStatus.value.validCount - val rejected = scanMatch.groupValues[3].toIntOrNull() ?: _scanStatus.value.rejectedCount + SCAN_TOTALS_REGEX.find(line)?.let { match -> + val resolver = match.groupValues[1] + val valid = match.groupValues[2].toIntOrNull() ?: _scanStatus.value.validCount + val rejected = match.groupValues[3].toIntOrNull() ?: _scanStatus.value.rejectedCount val decision = when { line.contains("Accepted", ignoreCase = true) -> "Accepted" line.contains("Rejected", ignoreCase = true) -> "Rejected" @@ -290,47 +341,31 @@ object VpnManager { return } - val activeResolversMatch = Regex( - "Active Resolvers\\s*[:=]\\s*[^\\d-]*(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (activeResolversMatch != null) { + ACTIVE_RESOLVERS_REGEX.find(line)?.let { match -> _scanStatus.value = _scanStatus.value.copy( - activeResolvers = activeResolversMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers + activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers ) return } - val totalActiveMatch = Regex( - "total\\s+active\\s*[:=]\\s*[^\\d-]*(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (totalActiveMatch != null) { + TOTAL_ACTIVE_REGEX.find(line)?.let { match -> _scanStatus.value = _scanStatus.value.copy( - activeResolvers = totalActiveMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers + activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers ) return } - val remainingMatch = Regex( - "remaining\\s*[:=]\\s*[^\\d-]*(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (remainingMatch != null) { + REMAINING_REGEX.find(line)?.let { match -> _scanStatus.value = _scanStatus.value.copy( - activeResolvers = remainingMatch.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers + activeResolvers = match.groupValues[1].toIntOrNull() ?: _scanStatus.value.activeResolvers ) return } - val syncedMatch = Regex( - "Selected Synced Upload MTU:\\s*(\\d+)\\s*\\|\\s*Selected Synced Download MTU:\\s*(\\d+)", - RegexOption.IGNORE_CASE - ).find(line) - if (syncedMatch != null) { + SYNCED_MTU_REGEX.find(line)?.let { match -> _scanStatus.value = _scanStatus.value.copy( - syncedUploadMtu = syncedMatch.groupValues[1].toIntOrNull() ?: 0, - syncedDownloadMtu = syncedMatch.groupValues[2].toIntOrNull() ?: 0 + syncedUploadMtu = match.groupValues[1].toIntOrNull() ?: 0, + syncedDownloadMtu = match.groupValues[2].toIntOrNull() ?: 0 ) return } @@ -348,49 +383,24 @@ object VpnManager { } private fun normalizeLogTimestampToLocal(line: String): String { - val candidates = listOf( - // Example: 2026-04-05T10:20:30.123Z - Triple( - Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)(.*)$"), - "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", - "yyyy-MM-dd HH:mm:ss.SSS" - ), - // Example: 2026-04-05T10:20:30Z - Triple( - Regex("^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)(.*)$"), - "yyyy-MM-dd'T'HH:mm:ss'Z'", - "yyyy-MM-dd HH:mm:ss" - ), - // Example: 2026-04-05 10:20:30 UTC (check UTC variant first) - Triple( - Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+UTC(.*)$"), - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm:ss" - ), - // Example: 2026-04-05 10:20:30 (from mtu_logging.go, no UTC) - Triple( - Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s(.*)$"), - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm:ss" - ), - // Example: 2026/04/05 10:20:30 (from logger.go) - Triple( - Regex("^(\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})(.*)$"), - "yyyy/MM/dd HH:mm:ss", - "yyyy/MM/dd HH:mm:ss" - ) - ) + if (!startsWithTimestamp(line)) return line - for ((regex, inputFormat, outputFormat) in candidates) { - val match = regex.find(line) ?: continue + for (candidate in TIMESTAMP_CANDIDATES) { + val match = candidate.regex.find(line) ?: continue val utcStamp = match.groupValues[1] val suffix = match.groupValues[2] - val localStamp = convertUtcToLocal(utcStamp, inputFormat, outputFormat) ?: continue + val localStamp = convertUtcToLocal(utcStamp, candidate.inputPattern, candidate.outputPattern) ?: continue return "$localStamp$suffix" } return line } + private fun startsWithTimestamp(line: String): Boolean { + if (line.length < 19) return false + if (!line[0].isDigit() || !line[1].isDigit() || !line[2].isDigit() || !line[3].isDigit()) return false + return (line[4] == '-' && line[7] == '-') || (line[4] == '/' && line[7] == '/') + } + private fun convertUtcToLocal( utcValue: String, inputPattern: String,