mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
fix: mobile data tunneling
Fixes a bug where mobile data tunneling was not working properly in certain scenarios. Fixes an issue where the new floating action button was not working correctly on AndroidTV. Improved local logging. Additional refactors and optimizations.
This commit is contained in:
@@ -40,4 +40,7 @@ dependencies {
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
// logging
|
||||
implementation(libs.timber)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.logcatter
|
||||
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.io.File
|
||||
|
||||
interface LocalLogCollector {
|
||||
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
|
||||
fun stop()
|
||||
suspend fun getLogFile(): Result<File>
|
||||
val bufferedLogs: Flow<LogMessage>
|
||||
}
|
||||
|
||||
@@ -1,34 +1,271 @@
|
||||
package com.zaneschepke.logcatter
|
||||
|
||||
import android.content.Context
|
||||
import com.zaneschepke.logcatter.model.LogLevel
|
||||
import com.zaneschepke.logcatter.model.LogMessage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
object Logcatter {
|
||||
object LogcatHelper {
|
||||
|
||||
private const val MAX_FILE_SIZE = 2097152L // 2MB
|
||||
private const val MAX_FOLDER_SIZE = 10485760L // 10MB
|
||||
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
|
||||
private val findIpv6AddressRegex = """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex()
|
||||
private val findIpv6AddressRegex =
|
||||
"""(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex()
|
||||
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
|
||||
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
|
||||
private const val CHORE = "Choreographer"
|
||||
|
||||
private object LogcatHelperInit {
|
||||
var maxFileSize: Long = MAX_FILE_SIZE
|
||||
var maxFolderSize: Long = MAX_FOLDER_SIZE
|
||||
var pID: Int = 0
|
||||
var publicAppDirectory = ""
|
||||
var logcatPath = ""
|
||||
}
|
||||
|
||||
fun logs(callback: (input: LogMessage) -> Unit, obfuscator: (log : String) -> String = { log -> this.obfuscator(log)}){
|
||||
clear()
|
||||
Runtime.getRuntime().exec("logcat -v epoch")
|
||||
.inputStream
|
||||
.bufferedReader()
|
||||
.useLines { lines ->
|
||||
lines.forEach { callback(LogMessage.from(obfuscator(it))) }
|
||||
fun init(
|
||||
maxFileSize: Long = MAX_FILE_SIZE,
|
||||
maxFolderSize: Long = MAX_FOLDER_SIZE,
|
||||
context: Context
|
||||
): LocalLogCollector {
|
||||
if (maxFileSize > maxFolderSize) {
|
||||
throw IllegalStateException("maxFileSize must be less than maxFolderSize")
|
||||
}
|
||||
synchronized(LogcatHelperInit) {
|
||||
LogcatHelperInit.maxFileSize = maxFileSize
|
||||
LogcatHelperInit.maxFolderSize = maxFolderSize
|
||||
LogcatHelperInit.pID = android.os.Process.myPid()
|
||||
context.getExternalFilesDir(null)?.let {
|
||||
LogcatHelperInit.publicAppDirectory = it.absolutePath
|
||||
LogcatHelperInit.logcatPath =
|
||||
LogcatHelperInit.publicAppDirectory + File.separator + "logs"
|
||||
val logDirectory = File(LogcatHelperInit.logcatPath)
|
||||
if (!logDirectory.exists()) {
|
||||
logDirectory.mkdir()
|
||||
}
|
||||
}
|
||||
return Logcat
|
||||
}
|
||||
}
|
||||
|
||||
private fun obfuscator(log : String) : String {
|
||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
||||
}
|
||||
}.let{ last -> findIpv4AddressRegex.replace(last,"<ipv4-address>") }
|
||||
}
|
||||
internal object Logcat : LocalLogCollector {
|
||||
|
||||
fun clear() {
|
||||
Runtime.getRuntime().exec("logcat -c")
|
||||
private var logcatReader: LogcatReader? = null
|
||||
|
||||
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
|
||||
logcatReader ?: run {
|
||||
logcatReader = LogcatReader(
|
||||
LogcatHelperInit.pID.toString(),
|
||||
LogcatHelperInit.logcatPath,
|
||||
onLogMessage,
|
||||
)
|
||||
}
|
||||
logcatReader?.let { logReader ->
|
||||
if (!logReader.isAlive) logReader.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
logcatReader?.stopLogs()
|
||||
logcatReader = null
|
||||
}
|
||||
|
||||
private fun mergeLogsApi26(sourceDir: String, outputFile: File) {
|
||||
val outputFilePath = Paths.get(outputFile.absolutePath)
|
||||
val logcatPath = Paths.get(sourceDir)
|
||||
|
||||
Files.list(logcatPath).use {
|
||||
it.sorted { o1, o2 ->
|
||||
Files.getLastModifiedTime(o1).compareTo(Files.getLastModifiedTime(o2))
|
||||
}
|
||||
.flatMap(Files::lines).use { lines ->
|
||||
lines.forEach { line ->
|
||||
Files.write(
|
||||
outputFilePath,
|
||||
(line + System.lineSeparator()).toByteArray(),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLogFile(): Result<File> {
|
||||
stop()
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val outputDir =
|
||||
File(LogcatHelperInit.publicAppDirectory + File.separator + "output")
|
||||
val outputFile = File(outputDir.absolutePath + File.separator + "logs.txt")
|
||||
|
||||
if (!outputDir.exists()) outputDir.mkdir()
|
||||
if (outputFile.exists()) outputFile.delete()
|
||||
|
||||
mergeLogsApi26(LogcatHelperInit.logcatPath, outputFile)
|
||||
Result.success(outputFile)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
} finally {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
|
||||
replay = 10_000,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
|
||||
|
||||
private class LogcatReader(
|
||||
pID: String,
|
||||
private val logcatPath: String,
|
||||
private val callback: ((input: LogMessage) -> Unit)?,
|
||||
) : Thread() {
|
||||
private var logcatProc: Process? = null
|
||||
private var reader: BufferedReader? = null
|
||||
private var mRunning = true
|
||||
private var command = ""
|
||||
private var clearLogCommand = ""
|
||||
private var outputStream: FileOutputStream? = null
|
||||
|
||||
init {
|
||||
try {
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
} catch (e: FileNotFoundException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
||||
command = "logcat -v epoch | grep \"($pID)\""
|
||||
clearLogCommand = "logcat -c"
|
||||
}
|
||||
|
||||
fun stopLogs() {
|
||||
mRunning = false
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
Runtime.getRuntime().exec(clearLogCommand)
|
||||
}
|
||||
|
||||
private fun obfuscator(log: String): String {
|
||||
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
|
||||
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
|
||||
findTunnelNameRegex.replace(second, "<tunnel>")
|
||||
}
|
||||
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (outputStream == null) return
|
||||
try {
|
||||
clear()
|
||||
logcatProc = Runtime.getRuntime().exec(command)
|
||||
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
|
||||
var line: String? = null
|
||||
|
||||
while (mRunning && run {
|
||||
line = reader!!.readLine()
|
||||
line
|
||||
} != null
|
||||
) {
|
||||
if (!mRunning) {
|
||||
break
|
||||
}
|
||||
if (line!!.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (outputStream!!.channel.size() >= LogcatHelperInit.maxFileSize) {
|
||||
outputStream!!.close()
|
||||
outputStream = FileOutputStream(createLogFile(logcatPath))
|
||||
}
|
||||
if (getFolderSize(logcatPath) >= LogcatHelperInit.maxFolderSize) {
|
||||
deleteOldestFile(logcatPath)
|
||||
}
|
||||
line?.let { text ->
|
||||
val obfuscated = obfuscator(text)
|
||||
outputStream!!.write((obfuscated + System.lineSeparator()).toByteArray())
|
||||
try {
|
||||
val logMessage = LogMessage.from(obfuscated)
|
||||
when (logMessage.level) {
|
||||
LogLevel.VERBOSE -> Unit
|
||||
else -> {
|
||||
if (!logMessage.tag.contains(CHORE)) {
|
||||
_bufferedLogs.tryEmit(logMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.let {
|
||||
it(logMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
logcatProc?.destroy()
|
||||
logcatProc = null
|
||||
|
||||
try {
|
||||
reader?.close()
|
||||
outputStream?.close()
|
||||
reader = null
|
||||
outputStream = null
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFolderSize(path: String): Long {
|
||||
File(path).run {
|
||||
var size = 0L
|
||||
if (this.isDirectory && this.listFiles() != null) {
|
||||
for (file in this.listFiles()!!) {
|
||||
size += getFolderSize(file.absolutePath)
|
||||
}
|
||||
} else {
|
||||
size = this.length()
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(dir: String): File {
|
||||
return File(dir, "logcat_" + System.currentTimeMillis() + ".txt")
|
||||
}
|
||||
|
||||
private fun deleteOldestFile(path: String) {
|
||||
val directory = File(path)
|
||||
if (directory.isDirectory) {
|
||||
directory.listFiles()?.toMutableList()?.run {
|
||||
this.sortBy { it.lastModified() }
|
||||
this.first().delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.zaneschepke.logcatter.model
|
||||
import java.time.Instant
|
||||
|
||||
data class LogMessage(
|
||||
val time: Instant,
|
||||
val time: String,
|
||||
val pid: String,
|
||||
val tid: String,
|
||||
val level: LogLevel,
|
||||
val tag: String,
|
||||
val message: String
|
||||
val message: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "$time $pid $tid $level $tag message= $message"
|
||||
@@ -16,21 +16,22 @@ data class LogMessage(
|
||||
|
||||
companion object {
|
||||
fun from(logcatLine: String): LogMessage {
|
||||
return if (logcatLine.contains("---------")) LogMessage(
|
||||
Instant.now(),
|
||||
"0",
|
||||
"0",
|
||||
LogLevel.VERBOSE,
|
||||
"System",
|
||||
logcatLine,
|
||||
)
|
||||
else {
|
||||
//TODO improve this
|
||||
return if (logcatLine.contains("---------")) {
|
||||
LogMessage(
|
||||
Instant.now().toString(),
|
||||
"0",
|
||||
"0",
|
||||
LogLevel.VERBOSE,
|
||||
"System",
|
||||
logcatLine,
|
||||
)
|
||||
} else {
|
||||
// TODO improve this
|
||||
val parts = logcatLine.trim().split(" ").filter { it.isNotEmpty() }
|
||||
val epochParts = parts[0].split(".").map { it.toLong() }
|
||||
val message = parts.subList(5, parts.size).joinToString(" ")
|
||||
LogMessage(
|
||||
Instant.ofEpochSecond(epochParts[0], epochParts[1]),
|
||||
Instant.ofEpochSecond(epochParts[0], epochParts[1]).toString(),
|
||||
parts[1],
|
||||
parts[2],
|
||||
LogLevel.fromSignifier(parts[3]),
|
||||
|
||||
Reference in New Issue
Block a user