Skip to content

Commit 8f2fd93

Browse files
committed
fix: improve local logging
1 parent 94197c9 commit 8f2fd93

File tree

8 files changed

+267
-261
lines changed

8 files changed

+267
-261
lines changed

app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
9696

9797
applicationScope.launch {
9898
withContext(mainDispatcher) {
99-
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
99+
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.start()
100100
}
101101
if (!appDataRepository.settings.get().isKernelEnabled) {
102102
tunnelManager.setBackendState(BackendState.SERVICE_ACTIVE, emptyList())

app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt

+2-19
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ import androidx.compose.material3.MaterialTheme
1818
import androidx.compose.material3.Text
1919
import androidx.compose.runtime.Composable
2020
import androidx.compose.runtime.getValue
21-
import androidx.compose.runtime.mutableStateOf
22-
import androidx.compose.runtime.remember
2321
import androidx.compose.runtime.setValue
2422
import androidx.compose.ui.Alignment
2523
import androidx.compose.ui.Modifier
@@ -36,7 +34,6 @@ import com.zaneschepke.wireguardautotunnel.ui.Route
3634
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
3735
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
3836
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
39-
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
4037
import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
4138
import com.zaneschepke.wireguardautotunnel.ui.common.label.VersionLabel
4239
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
@@ -51,20 +48,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
5148
fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
5249
val context = LocalContext.current
5350
val navController = LocalNavController.current
54-
55-
var showDialog by remember { mutableStateOf(false) }
56-
57-
if (showDialog) {
58-
InfoDialog(onAttest = {
59-
showDialog = false
60-
appViewModel.onToggleLocalLogging()
61-
}, onDismiss = {
62-
showDialog = false
63-
}, title = {
64-
Text(stringResource(R.string.configuration_change))
65-
}, body = { Text(stringResource(R.string.requires_app_relaunch)) }, confirmText = { Text(stringResource(R.string.yes)) })
66-
}
67-
6851
Column(
6952
horizontalAlignment = Alignment.Start,
7053
verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top),
@@ -115,12 +98,12 @@ fun SupportScreen(appUiState: AppUiState, appViewModel: AppViewModel) {
11598
ScaledSwitch(
11699
appUiState.generalState.isLocalLogsEnabled,
117100
onClick = {
118-
showDialog = true
101+
appViewModel.onToggleLocalLogging()
119102
},
120103
)
121104
},
122105
onClick = {
123-
showDialog = true
106+
appViewModel.onToggleLocalLogging()
124107
},
125108
),
126109
)

app/src/main/java/com/zaneschepke/wireguardautotunnel/viewmodel/AppViewModel.kt

+2-7
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,12 @@ constructor(
145145
with(uiState.value.generalState) {
146146
val toggledOn = !isLocalLogsEnabled
147147
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
148-
if (!toggledOn) onLoggerStop()
149-
_configurationChange.update {
150-
true
148+
if (!toggledOn) {
149+
logReader.stop()
151150
}
152151
}
153152
}
154153

155-
private suspend fun onLoggerStop() {
156-
logReader.deleteAndClearLogs()
157-
}
158-
159154
fun onToggleAlwaysOnVPN() = viewModelScope.launch {
160155
with(uiState.value.appSettings) {
161156
appDataRepository.settings.save(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.zaneschepke.logcatter
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.withContext
5+
import java.io.BufferedOutputStream
6+
import java.io.File
7+
import java.io.FileOutputStream
8+
import java.util.zip.ZipEntry
9+
import java.util.zip.ZipOutputStream
10+
11+
class LogFileManager(
12+
private val logDir: String,
13+
private val maxFileSize: Long,
14+
private val maxFolderSize: Long,
15+
) {
16+
private var currentFile: File? = null
17+
private var outputStream: FileOutputStream? = null
18+
19+
val ioDispatcher = Dispatchers.IO
20+
21+
init {
22+
rotateIfNeeded()
23+
}
24+
25+
suspend fun writeLog(line: String) = withContext(ioDispatcher) {
26+
rotateIfNeeded()
27+
outputStream?.write((line + System.lineSeparator()).toByteArray())
28+
outputStream?.flush()
29+
}
30+
31+
suspend fun zipLogs(zipFilePath: String) = withContext(ioDispatcher) {
32+
outputStream?.close()
33+
val sourceDir = File(logDir)
34+
if (!sourceDir.exists() || !sourceDir.isDirectory) return@withContext
35+
val outputZipFile = File(zipFilePath)
36+
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
37+
sourceDir.walkTopDown().forEach { file ->
38+
val zipFileName = file.absolutePath.removePrefix(sourceDir.absolutePath).removePrefix("/")
39+
val entry = ZipEntry("$zipFileName${if (file.isDirectory) "/" else ""}")
40+
zos.putNextEntry(entry)
41+
if (file.isFile) {
42+
file.inputStream().use { it.copyTo(zos) }
43+
}
44+
}
45+
}
46+
rotateIfNeeded()
47+
}
48+
49+
suspend fun deleteAllLogs() = withContext(ioDispatcher) {
50+
outputStream?.close()
51+
File(logDir).listFiles()?.forEach { it.delete() }
52+
rotateIfNeeded()
53+
}
54+
55+
fun close() {
56+
outputStream?.close()
57+
outputStream = null
58+
currentFile = null
59+
}
60+
61+
private fun rotateIfNeeded() {
62+
val folderSize = getFolderSize(File(logDir))
63+
if (folderSize >= maxFolderSize) {
64+
deleteOldestFile()
65+
}
66+
val fileSize = currentFile?.length() ?: 0L
67+
if (currentFile == null || fileSize >= maxFileSize) {
68+
outputStream?.close()
69+
currentFile = File(logDir, "logcat_${System.currentTimeMillis()}.txt")
70+
outputStream = FileOutputStream(currentFile!!)
71+
}
72+
}
73+
74+
private fun getFolderSize(dir: File): Long {
75+
var size = 0L
76+
if (dir.isDirectory && dir.listFiles() != null) {
77+
dir.listFiles()!!.forEach { file ->
78+
size += if (file.isDirectory) getFolderSize(file) else file.length()
79+
}
80+
}
81+
return size
82+
}
83+
84+
private fun deleteOldestFile() {
85+
File(logDir).listFiles()
86+
?.toList()
87+
?.minByOrNull { it.lastModified() }
88+
?.delete()
89+
}
90+
}

logcatter/src/main/java/com/zaneschepke/logcatter/LogReader.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import com.zaneschepke.logcatter.model.LogMessage
44
import kotlinx.coroutines.flow.Flow
55

66
interface LogReader {
7-
fun initialize(onLogMessage: ((message: LogMessage) -> Unit)? = null)
7+
fun start()
8+
fun stop()
89
fun zipLogFiles(path: String)
910
suspend fun deleteAndClearLogs()
1011
val bufferedLogs: Flow<LogMessage>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.zaneschepke.logcatter
2+
3+
import androidx.lifecycle.DefaultLifecycleObserver
4+
import androidx.lifecycle.LifecycleOwner
5+
import com.zaneschepke.logcatter.model.LogMessage
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.ExperimentalCoroutinesApi
9+
import kotlinx.coroutines.Job
10+
import kotlinx.coroutines.SupervisorJob
11+
import kotlinx.coroutines.cancel
12+
import kotlinx.coroutines.channels.BufferOverflow
13+
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.MutableSharedFlow
15+
import kotlinx.coroutines.flow.asSharedFlow
16+
import kotlinx.coroutines.launch
17+
18+
class LogcatManager(
19+
pid: Int,
20+
logDir: String,
21+
maxFileSize: Long,
22+
maxFolderSize: Long,
23+
) : LogReader, DefaultLifecycleObserver {
24+
private val logScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
25+
private val fileManager = LogFileManager(logDir, maxFileSize, maxFolderSize)
26+
private val logcatReader = LogcatStreamReader(pid, fileManager)
27+
private var logJob: Job? = null
28+
private var isStarted = false
29+
30+
private val _bufferedLogs = MutableSharedFlow<LogMessage>(
31+
replay = 10_000,
32+
onBufferOverflow = BufferOverflow.DROP_OLDEST,
33+
)
34+
private val _liveLogs = MutableSharedFlow<LogMessage>(
35+
replay = 1,
36+
onBufferOverflow = BufferOverflow.DROP_OLDEST,
37+
)
38+
39+
override val bufferedLogs: Flow<LogMessage> = _bufferedLogs.asSharedFlow()
40+
override val liveLogs: Flow<LogMessage> = _liveLogs.asSharedFlow()
41+
42+
override fun onCreate(owner: LifecycleOwner) {
43+
// for auto start
44+
// start()
45+
}
46+
47+
override fun onDestroy(owner: LifecycleOwner) {
48+
stop()
49+
logScope.cancel()
50+
}
51+
52+
override fun start() {
53+
if (isStarted) return
54+
stop()
55+
logJob = logScope.launch {
56+
logcatReader.readLogs().collect { logMessage ->
57+
_bufferedLogs.emit(logMessage)
58+
_liveLogs.emit(logMessage)
59+
}
60+
}
61+
isStarted = true
62+
}
63+
64+
override fun stop() {
65+
if (!isStarted) return
66+
logJob?.cancel()
67+
logcatReader.stop()
68+
fileManager.close()
69+
isStarted = false
70+
}
71+
72+
override fun zipLogFiles(path: String) {
73+
logScope.launch {
74+
val wasStarted = isStarted
75+
stop()
76+
fileManager.zipLogs(path)
77+
if (wasStarted) {
78+
logcatReader.clearLogs()
79+
start()
80+
}
81+
}
82+
}
83+
84+
@OptIn(ExperimentalCoroutinesApi::class)
85+
override suspend fun deleteAndClearLogs() {
86+
val wasStarted = isStarted
87+
stop()
88+
_bufferedLogs.resetReplayCache()
89+
fileManager.deleteAllLogs()
90+
if (wasStarted) start()
91+
}
92+
}

0 commit comments

Comments
 (0)