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

View File

@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector
import com.zaneschepke.logcatter.LogcatReader
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,6 +25,6 @@ class AppModule {
@Singleton
@Provides
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.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

View File

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

View File

@ -50,7 +50,7 @@ fun LanguageScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
modifier =
Modifier
.fillMaxSize().padding(padding)
.padding(horizontal = 24.dp.scaledWidth())
.padding(horizontal = 24.dp.scaledWidth()),
) {
item {
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.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding

View File

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

View File

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

View File

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