fix: make logging lifecycle aware
This commit is contained in:
parent
bbfc0e2fab
commit
670d9d680c
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -141,7 +141,6 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onLoggerStop() {
|
private suspend fun onLoggerStop() {
|
||||||
logReader.stop()
|
|
||||||
logReader.deleteAndClearLogs()
|
logReader.deleteAndClearLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue