fix: make logging lifecycle aware

This commit is contained in:
Zane Schepke 2024-12-09 21:41:59 -05:00
parent bbfc0e2fab
commit 670d9d680c
9 changed files with 78 additions and 101 deletions

View File

@ -8,6 +8,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState import com.zaneschepke.wireguardautotunnel.service.tunnel.BackendState
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.LocaleUtil import com.zaneschepke.wireguardautotunnel.util.LocaleUtil
@ -17,6 +18,7 @@ import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -43,6 +45,10 @@ class WireGuardAutoTunnel : Application() {
@IoDispatcher @IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher lateinit var ioDispatcher: CoroutineDispatcher
@Inject
@MainDispatcher
lateinit var mainDispatcher: CoroutineDispatcher
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
@ -59,22 +65,19 @@ class WireGuardAutoTunnel : Application() {
} else { } else {
Timber.plant(ReleaseTree()) Timber.plant(ReleaseTree())
} }
applicationScope.launch { applicationScope.launch {
withContext(mainDispatcher) {
if (appStateRepository.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
}
if (!settingsRepository.getSettings().isKernelEnabled) { if (!settingsRepository.getSettings().isKernelEnabled) {
tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList()) tunnelService.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())
} }
appStateRepository.getLocale()?.let { appStateRepository.getLocale()?.let {
LocaleUtil.changeLocale(it) LocaleUtil.changeLocale(it)
} }
} }
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
if (appStateRepository.isLocalLogsEnabled()) {
Timber.d("Starting logger")
logReader.start()
}
}
}
} }
override fun onTerminate() { override fun onTerminate() {

View File

@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context import android.content.Context
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector import com.zaneschepke.logcatter.LogcatReader
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -25,6 +25,6 @@ class AppModule {
@Singleton @Singleton
@Provides @Provides
fun provideLogCollect(@ApplicationContext context: Context): LogReader { fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatCollector.init(context = context) return LogcatReader.init(storageDir = context.filesDir.absolutePath)
} }
} }

View File

@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@ -141,7 +141,6 @@ constructor(
} }
private suspend fun onLoggerStop() { private suspend fun onLoggerStop() {
logReader.stop()
logReader.deleteAndClearLogs() logReader.deleteAndClearLogs()
} }

View File

@ -50,7 +50,7 @@ fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
modifier = modifier =
Modifier Modifier
.fillMaxSize().padding(padding) .fillMaxSize().padding(padding)
.padding(horizontal = 24.dp.scaledWidth()) .padding(horizontal = 24.dp.scaledWidth()),
) { ) {
item { item {
Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) { Box(modifier = Modifier.padding(top = 24.dp.scaledHeight())) {

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding

View File

@ -49,6 +49,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.androidx.lifecycle.process)
// logging // logging
implementation(libs.timber) implementation(libs.timber)
} }

View File

@ -4,8 +4,7 @@ import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface LogReader { interface LogReader {
suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null) fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop()
fun zipLogFiles(path: String) fun zipLogFiles(path: String)
suspend fun deleteAndClearLogs() suspend fun deleteAndClearLogs()
val bufferedLogs: Flow<LogMessage> val bufferedLogs: Flow<LogMessage>

View File

@ -1,26 +1,30 @@
package com.zaneschepke.logcatter package com.zaneschepke.logcatter
import android.content.Context import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
object LogcatCollector { object LogcatReader {
private const val MAX_FILE_SIZE = 2097152L // 2MB private const val MAX_FILE_SIZE = 2097152L // 2MB
private const val MAX_FOLDER_SIZE = 10485760L // 10MB private const val MAX_FOLDER_SIZE = 10485760L // 10MB
@ -40,7 +44,7 @@ object LogcatCollector {
var logcatPath = "" var logcatPath = ""
} }
fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, context: Context): LogReader { fun init(maxFileSize: Long = MAX_FILE_SIZE, maxFolderSize: Long = MAX_FOLDER_SIZE, storageDir: String): LogReader {
if (maxFileSize > maxFolderSize) { if (maxFileSize > maxFolderSize) {
throw IllegalStateException("maxFileSize must be less than maxFolderSize") throw IllegalStateException("maxFileSize must be less than maxFolderSize")
} }
@ -48,13 +52,11 @@ object LogcatCollector {
LogcatHelperInit.maxFileSize = maxFileSize LogcatHelperInit.maxFileSize = maxFileSize
LogcatHelperInit.maxFolderSize = maxFolderSize LogcatHelperInit.maxFolderSize = maxFolderSize
LogcatHelperInit.pID = android.os.Process.myPid() LogcatHelperInit.pID = android.os.Process.myPid()
context.getExternalFilesDir(null)?.let { LogcatHelperInit.publicAppDirectory = storageDir
LogcatHelperInit.publicAppDirectory = it.absolutePath LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs"
LogcatHelperInit.logcatPath = LogcatHelperInit.publicAppDirectory + File.separator + "logs" val logDirectory = File(LogcatHelperInit.logcatPath)
val logDirectory = File(LogcatHelperInit.logcatPath) if (!logDirectory.exists()) {
if (!logDirectory.exists()) { logDirectory.mkdir()
logDirectory.mkdir()
}
} }
return Logcat return Logcat
} }
@ -62,7 +64,12 @@ object LogcatCollector {
internal object Logcat : LogReader { internal object Logcat : LogReader {
private var logcatReader: LogcatReader? = null private lateinit var logcatReader: LogcatReader
override fun initialize(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
ProcessLifecycleOwner.get().lifecycle.addObserver(logcatReader)
}
private fun obfuscator(log: String): String { private fun obfuscator(log: String): String {
return findKeyRegex.replace(log, "<crypto-key>").let { first -> return findKeyRegex.replace(log, "<crypto-key>").let { first ->
@ -72,22 +79,10 @@ object LogcatCollector {
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") } }.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
} }
override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run {
logcatReader = LogcatReader(LogcatHelperInit.pID.toString(), LogcatHelperInit.logcatPath, onLogMessage)
}
logcatReader?.run()
}
override fun stop() {
logcatReader?.stop()
logcatReader = null
}
override fun zipLogFiles(path: String) { override fun zipLogFiles(path: String) {
logcatReader?.pause() logcatReader.cancel()
zipAll(path) zipAll(path)
logcatReader?.resume() logcatReader.onCreate(ProcessLifecycleOwner.get())
} }
private fun zipAll(zipFilePath: String) { private fun zipAll(zipFilePath: String) {
@ -110,10 +105,10 @@ object LogcatCollector {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun deleteAndClearLogs() { override suspend fun deleteAndClearLogs() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
logcatReader?.pause() logcatReader.cancel()
_bufferedLogs.resetReplayCache() _bufferedLogs.resetReplayCache()
logcatReader?.deleteAllFiles() logcatReader.deleteAllFiles()
logcatReader?.resume() logcatReader.onCreate(ProcessLifecycleOwner.get())
} }
} }
@ -134,57 +129,25 @@ object LogcatCollector {
pID: String, pID: String,
private val logcatPath: String, private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?, private val callback: ((input: LogMessage) -> Unit)?,
) { ) : DefaultLifecycleObserver {
private var logcatProc: Process? = null private var logcatProc: Process? = null
private var reader: BufferedReader? = null private var reader: BufferedReader? = null
@get:Synchronized @set:Synchronized private val command = "logcat -v epoch | grep \"($pID)\""
private var paused = false private val clearLogCommand = "logcat -c"
private var logJob: Job? = null
@get:Synchronized @set:Synchronized
private var stopped = false
private var command = ""
private var clearLogCommand = ""
private var outputStream: FileOutputStream? = null private var outputStream: FileOutputStream? = null
init { override fun onCreate(owner: LifecycleOwner) {
try { super.onCreate(owner)
outputStream = FileOutputStream(createLogFile(logcatPath)) logJob = owner.lifecycleScope.launch(ioDispatcher) {
} catch (e: FileNotFoundException) {
Timber.e(e)
}
command = "logcat -v epoch | grep \"($pID)\""
clearLogCommand = "logcat -c"
}
fun pause() {
paused = true
}
fun stop() {
stopped = true
}
fun resume() {
paused = false
}
fun clear() {
Runtime.getRuntime().exec(clearLogCommand)
}
suspend fun run() {
withContext(ioDispatcher) {
paused = false
stopped = false
if (outputStream == null) return@withContext
try { try {
if (outputStream == null) outputStream = createNewLogFileStream()
clear() clear()
logcatProc = Runtime.getRuntime().exec(command) logcatProc = Runtime.getRuntime().exec(command)
reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024) reader = BufferedReader(InputStreamReader(logcatProc!!.inputStream), 1024)
var line: String? var line: String? = null
while (!stopped) { while (true) {
if (paused) continue
line = reader?.readLine() line = reader?.readLine()
if (line.isNullOrEmpty()) continue if (line.isNullOrEmpty()) continue
outputStream?.let { outputStream?.let {
@ -196,8 +159,8 @@ object LogcatCollector {
deleteOldestFile() deleteOldestFile()
} }
line.let { text -> line.let { text ->
val obfuscated = obfuscator(text) val sanitized = obfuscator(text)
it.write((obfuscated + System.lineSeparator()).toByteArray()) it.write((sanitized + System.lineSeparator()).toByteArray())
try { try {
val logMessage = LogMessage.from(text) val logMessage = LogMessage.from(text)
_bufferedLogs.tryEmit(logMessage) _bufferedLogs.tryEmit(logMessage)
@ -214,19 +177,34 @@ object LogcatCollector {
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} finally { } finally {
logcatProc?.destroy() reset()
logcatProc = null
try {
reader?.close()
outputStream?.close()
reader = null
outputStream = null
} catch (e: IOException) {
Timber.e(e)
}
} }
} }
logJob?.invokeOnCompletion {
reset()
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
logJob?.cancel()
}
fun cancel() {
logJob?.cancel()
}
private fun reset() {
logcatProc?.destroy()
logcatProc = null
reader?.close()
outputStream?.close()
reader = null
outputStream = null
}
fun clear() {
Runtime.getRuntime().exec(clearLogCommand)
} }
private fun getFolderSize(path: String): Long { private fun getFolderSize(path: String): Long {
@ -266,7 +244,6 @@ object LogcatCollector {
directory.listFiles()?.toMutableList()?.run { directory.listFiles()?.toMutableList()?.run {
this.forEach { it.delete() } this.forEach { it.delete() }
} }
outputStream = createNewLogFileStream()
} }
} }
} }