add in app log viewer

This commit is contained in:
qimiko committed 2024-01-20 06:20:50 -07:00
1 parent 05eb3af243
commit 397cb9b9bd
8 files changed
+593 -18

No files matched your search

@@ -0,0 +1,184 @@
package com.geode.launcher.log
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime
import java.io.DataInputStream
import java.io.IOException
fun DataInputStream.readCChar(): Char {
return this.readByte().toInt().toChar()
}
fun DataInputStream.readCString(): String {
val buffer = StringBuilder()
var lastByte = this.readCChar()
while (lastByte != '\u0000') {
buffer.append(lastByte)
lastByte = this.readCChar()
}
return buffer.toString()
}
fun DataInputStream.readLeUShort(): UShort {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val shortValue = (byteB shl 8) or byteA
return shortValue.toUShort()
}
fun DataInputStream.readLeInt(): Int {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val byteC = this.readByte().toInt() and 0xff
val byteD = this.readByte().toInt() and 0xff
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
fun DataInputStream.readLeUInt(): UInt {
val byteA = this.readByte().toUInt() and 0xffu
val byteB = this.readByte().toUInt() and 0xffu
val byteC = this.readByte().toUInt() and 0xffu
val byteD = this.readByte().toUInt() and 0xffu
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
companion object {
fun fromByte(byte: Byte): LogPriority {
return when (byte.toInt()) {
0x1 -> DEFAULT
0x2 -> VERBOSE
0x3 -> DEBUG
0x4 -> INFO
0x5 -> WARN
0x6 -> ERROR
0x7 -> FATAL
0x8 -> SILENT
else -> UNKNOWN
}
}
}
fun toChar(): Char {
return when (this) {
VERBOSE -> 'V'
DEBUG -> 'D'
INFO -> 'I'
WARN -> 'W'
ERROR -> 'E'
FATAL -> 'F'
SILENT -> 'S'
else -> '?'
}
}
}
/**
* Represents a log entry from logcat.
*/
data class LogLine(
val process: ProcessInformation,
val time: Instant,
val logId: Int,
val priority: LogPriority,
val tag: String,
val message: String
) {
companion object {
private enum class EntryVersion {
V3, V4
}
private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
0x18 -> EntryVersion.V3
0x1c -> EntryVersion.V4
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
}
fun fromInputStream(readStream: DataInputStream): LogLine {
/*
// from android <liblog/include/log/log_read.h>
// there are multiple logger entry formats
// use the header_size to determine the one you have
struct logger_entry_v3 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v3)
int32_t pid; // generating process's pid
int32_t tid; // generating process's tid
int32_t sec; // seconds since Epoch
int32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload
}
struct logger_entry_v4 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
int32_t pid; // generating process's pid
uint32_t tid; // generating process's tid
uint32_t sec; // seconds since Epoch
uint32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload, bottom 4 bits currently
uint32_t uid; // generating process's uid
};
*/
/* val payloadLength = */ readStream.readLeUShort()
val headerSize = readStream.readLeUShort()
val entryVersion = headerSizeToVersion(headerSize)
val pid = readStream.readLeInt()
val tid = readStream.readLeUInt().toInt()
val sec = readStream.readLeUInt().toLong()
val nSec = readStream.readLeUInt().toInt()
val lid = readStream.readLeUInt().toInt()
val uid = if (entryVersion == EntryVersion.V4)
readStream.readLeUInt().toInt() else 0
val processInformation = ProcessInformation(pid, tid, uid)
val time = Instant.fromEpochSeconds(sec, nSec)
val priorityByte = readStream.readByte()
val priority = LogPriority.fromByte(priorityByte)
val tag = readStream.readCString()
val message = readStream.readCString()
return LogLine(
process = processInformation,
priority = priority,
time = time,
logId = lid,
tag = tag,
message = message
)
}
fun showException(exception: Exception) = LogLine(
process = ProcessInformation(0, 0, 0),
time = Clock.System.now(),
logId = 0,
priority = LogPriority.FATAL,
tag = "GeodeLauncher",
message = "Failed to parse log entry with ${exception.stackTraceToString()}"
)
}
val identifier = time.toJavaInstant()
val formattedTime = this.time.toLocalDateTime(TimeZone.currentSystemDefault())
val asSimpleString = "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.message}"
}
@@ -0,0 +1,86 @@
package com.geode.launcher.log
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import java.io.DataInputStream
import java.io.EOFException
import java.util.LinkedList
/**
* ViewModel to manage log output from logcat.
* Currently, it loads the list of logs at init with no ability to read additional logs.
*/
class LogViewModel: ViewModel() {
// the original version of this code did support streaming btw
// the states were way too annoying to manage across multiple threads
// yay!
private val _lineState = mutableStateListOf<LogLine>()
val lineState = _lineState
private var _isLoading = MutableStateFlow(false)
var isLoading = _isLoading.asStateFlow()
private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>()
val logStream = DataInputStream(
// -B = binary format, -d = dump logs
Runtime.getRuntime().exec("logcat -B -d").inputStream
)
try {
coroutineScope {
// this runs until the stream is exhausted
while (true) {
val line = LogLine.fromInputStream(logStream)
// skip debug/verbose lines
if (line.priority >= LogPriority.INFO) {
logLines += line
}
yield()
}
}
} catch (_: EOFException) {
// ignore, end of file reached
} catch (e: Exception) {
e.printStackTrace()
logLines += LogLine.showException(e)
}
return logLines
}
fun clearLogs() {
viewModelScope.launch(Dispatchers.IO) {
// -c = clear
Runtime.getRuntime().exec("logcat -c")
.waitFor()
}
_lineState.clear()
}
fun getLogData(): String {
return _lineState.joinToString("\n") { it.asSimpleString }
}
init {
_isLoading.value = true
viewModelScope.launch(Dispatchers.IO) {
val lines = logOutput()
_lineState.addAll(lines)
_isLoading.value = false
}
}
}