LogManager.kt (9150B) - raw


      1 package me.rhunk.snapenhance
      2 
      3 import android.util.Log
      4 import com.google.gson.GsonBuilder
      5 import me.rhunk.snapenhance.common.data.FileType
      6 import me.rhunk.snapenhance.common.logger.AbstractLogger
      7 import me.rhunk.snapenhance.common.logger.LogChannel
      8 import me.rhunk.snapenhance.common.logger.LogLevel
      9 import java.io.File
     10 import java.io.OutputStream
     11 import java.io.RandomAccessFile
     12 import java.time.format.DateTimeFormatter
     13 import java.util.zip.ZipEntry
     14 import java.util.zip.ZipOutputStream
     15 import kotlin.time.Duration.Companion.hours
     16 
     17 class LogLine(
     18     val logLevel: LogLevel,
     19     val dateTime: String,
     20     val tag: String,
     21     val message: String
     22 ) {
     23     companion object {
     24         fun fromString(line: String) = runCatching {
     25             val parts = line.trimEnd().split("/")
     26             if (parts.size != 4) return@runCatching null
     27             LogLine(
     28                 LogLevel.fromLetter(parts[0]) ?: return@runCatching null,
     29                 parts[1],
     30                 parts[2],
     31                 parts[3]
     32             )
     33         }.getOrNull()
     34     }
     35 
     36     override fun toString(): String {
     37         return "${logLevel.letter}/$dateTime/$tag/$message"
     38     }
     39 }
     40 
     41 
     42 class LogReader(
     43     logFile: File
     44 ) {
     45     private val randomAccessFile = RandomAccessFile(logFile, "r")
     46     private var startLineIndexes = mutableListOf<Long>()
     47     var lineCount = queryLineCount()
     48 
     49     private fun readLogLine(): LogLine? {
     50         val lines = StringBuilder()
     51         val lastPointer = randomAccessFile.filePointer
     52         var lastChar: Int = -1
     53         var bufferLength = 0
     54         while (true) {
     55             val char = randomAccessFile.read()
     56             if (char == -1) {
     57                 randomAccessFile.seek(lastPointer)
     58                 return null
     59             }
     60             if ((char == '|'.code && lastChar == '\n'.code) || bufferLength > 4096) {
     61                 break
     62             }
     63             lines.append(char.toChar())
     64             bufferLength++
     65             lastChar = char
     66         }
     67 
     68         return LogLine.fromString(lines.trimEnd().toString())
     69             ?: LogLine(LogLevel.ERROR, "1970-01-01 00:00:00", "LogReader", "Failed to parse log line: $lines")
     70     }
     71 
     72     fun incrementLineCount() {
     73         synchronized(randomAccessFile) {
     74             randomAccessFile.seek(randomAccessFile.length())
     75             startLineIndexes.add(randomAccessFile.filePointer + 1)
     76             lineCount++
     77         }
     78     }
     79 
     80     private fun queryLineCount(): Int {
     81         val buffer = ByteArray(1024 * 1024)
     82 
     83         synchronized(randomAccessFile) {
     84             randomAccessFile.seek(0)
     85             var lineCount = 0
     86             var read: Int
     87             var lastPointer: Long = 0
     88             var line: StringBuilder? = null
     89 
     90             while (randomAccessFile.read(buffer).also { read = it } != -1) {
     91                 for (i in 0 until read) {
     92                     val char = buffer[i].toInt().toChar()
     93                     if (line == null) {
     94                         line = StringBuilder()
     95                         lastPointer = randomAccessFile.filePointer - read + i
     96                     }
     97                     line.append(char)
     98                     if (char == '\n') {
     99                         if (line.startsWith('|')) {
    100                             lineCount++
    101                             startLineIndexes.add(lastPointer + 1)
    102                         }
    103                         line = null
    104                     }
    105                 }
    106             }
    107 
    108             return lineCount
    109         }
    110     }
    111 
    112     private fun getLine(index: Int): String? {
    113         if (index <= 0 || index > lineCount) return null
    114         synchronized(randomAccessFile) {
    115             randomAccessFile.seek(startLineIndexes.getOrNull(index) ?: return null)
    116             return readLogLine()?.toString()
    117         }
    118     }
    119 
    120     fun getLogLine(index: Int): LogLine? {
    121         return getLine(index)?.let { LogLine.fromString(it) }
    122     }
    123 }
    124 
    125 
    126 class LogManager(
    127     private val remoteSideContext: RemoteSideContext
    128 ): AbstractLogger(LogChannel.MANAGER) {
    129     companion object {
    130         private val LOG_LIFETIME = 24.hours
    131     }
    132 
    133     private val printLogLock = Any()
    134     private val anonymizeLogs by lazy { !remoteSideContext.config.root.scripting.disableLogAnonymization.get() }
    135 
    136     var lineAddListener = { _: LogLine -> }
    137 
    138     private val logFolder = File(remoteSideContext.androidContext.cacheDir, "logs")
    139     private var logFile: File? = null
    140 
    141     private val uuidRegex by lazy { Regex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", RegexOption.MULTILINE) }
    142     private val contentUriRegex by lazy { Regex("content://[a-zA-Z0-9_\\-./]+") }
    143     private val filePathRegex by lazy { Regex("([a-zA-Z0-9_\\-./]+)\\.(${FileType.entries.joinToString("|") { file -> file.fileExtension.toString() }})") }
    144 
    145     fun init() {
    146         if (!logFolder.exists()) {
    147             logFolder.mkdirs()
    148         }
    149         logFile = remoteSideContext.sharedPreferences.getString("log_file", null)?.let { File(it) }?.takeIf { it.exists() } ?: run {
    150             newLogFile()
    151             logFile
    152         }
    153 
    154         if (System.currentTimeMillis() - remoteSideContext.sharedPreferences.getLong("last_created", 0) > LOG_LIFETIME.inWholeMilliseconds) {
    155             newLogFile()
    156         }
    157     }
    158 
    159     fun internalLog(tag: String, logLevel: LogLevel, message: Any?) {
    160         synchronized(printLogLock) {
    161             runCatching {
    162                 val anonymizedMessage = message.toString().let {
    163                     if (remoteSideContext.config.isInitialized() && anonymizeLogs)
    164                         it.replace(uuidRegex, "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
    165                             .replace(contentUriRegex, "content://xxx")
    166                             .replace(filePathRegex, "xxxxxxxx.$2")
    167                     else it
    168                 }
    169                 val line = LogLine(
    170                     logLevel = logLevel,
    171                     dateTime = getCurrentDateTime(),
    172                     tag = tag,
    173                     message = anonymizedMessage
    174                 )
    175                 logFile?.appendText("|$line\n", Charsets.UTF_8)
    176                 lineAddListener(line)
    177                 Log.println(logLevel.priority, tag, anonymizedMessage)
    178             }.onFailure {
    179                 Log.println(Log.ERROR, tag, "Failed to log message: $message")
    180                 Log.println(Log.ERROR, tag, it.stackTraceToString())
    181             }
    182         }
    183     }
    184 
    185     private fun getCurrentDateTime(pathSafe: Boolean = false): String {
    186         return DateTimeFormatter.ofPattern(if (pathSafe) "yyyy-MM-dd_HH-mm-ss" else "yyyy-MM-dd HH:mm:ss").format(
    187             java.time.LocalDateTime.now()
    188         )
    189     }
    190 
    191     private fun newLogFile() {
    192         val currentTime = System.currentTimeMillis()
    193         logFile = File(logFolder, "snapenhance_${getCurrentDateTime(pathSafe = true)}.log").also {
    194             it.createNewFile()
    195             remoteSideContext.sharedPreferences.edit().putString("log_file", it.absolutePath).putLong("last_created", currentTime).apply()
    196         }
    197     }
    198 
    199     fun clearLogs() {
    200         logFolder.listFiles()?.forEach { it.delete() }
    201         newLogFile()
    202     }
    203 
    204     fun exportLogsToZip(outputStream: OutputStream) {
    205         val zipOutputStream = ZipOutputStream(outputStream).apply {
    206             setMethod(ZipOutputStream.DEFLATED)
    207         }
    208 
    209         // add device info to zip
    210         zipOutputStream.putNextEntry(ZipEntry("device_info.json"))
    211         val gson = GsonBuilder().setPrettyPrinting().create()
    212         zipOutputStream.write(gson.toJson(remoteSideContext.installationSummary).toByteArray())
    213         zipOutputStream.closeEntry()
    214 
    215         // add config
    216         zipOutputStream.putNextEntry(ZipEntry("config.json"))
    217         zipOutputStream.write(remoteSideContext.config.exportToString(exportSensitiveData = false).toByteArray())
    218         zipOutputStream.closeEntry()
    219 
    220         //add logFolder to zip
    221         logFolder.walk().forEach {
    222             if (it.isFile) {
    223                 zipOutputStream.putNextEntry(ZipEntry(it.name))
    224                 it.inputStream().copyTo(zipOutputStream)
    225                 zipOutputStream.closeEntry()
    226             }
    227         }
    228 
    229         zipOutputStream.close()
    230     }
    231 
    232     fun newReader(onAddLine: (LogLine) -> Unit) = LogReader(logFile!!).also {
    233         lineAddListener = { line -> it.incrementLineCount(); onAddLine(line) }
    234     }
    235 
    236     override fun debug(message: Any?, tag: String) {
    237         internalLog(tag, LogLevel.DEBUG, message)
    238     }
    239 
    240     override fun error(message: Any?, tag: String) {
    241         internalLog(tag, LogLevel.ERROR, message)
    242     }
    243 
    244     override fun error(message: Any?, throwable: Throwable, tag: String) {
    245         internalLog(tag, LogLevel.ERROR, message)
    246         internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString())
    247     }
    248 
    249     override fun info(message: Any?, tag: String) {
    250         internalLog(tag, LogLevel.INFO, message)
    251     }
    252 
    253     override fun verbose(message: Any?, tag: String) {
    254         internalLog(tag, LogLevel.VERBOSE, message)
    255     }
    256 
    257     override fun warn(message: Any?, tag: String) {
    258         internalLog(tag, LogLevel.WARN, message)
    259     }
    260 
    261     override fun assert(message: Any?, tag: String) {
    262         internalLog(tag, LogLevel.ASSERT, message)
    263     }
    264 }