From d7160db53a1177867a187063574270b560a2f6d7 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Fri, 22 Nov 2024 19:02:38 +0700 Subject: [PATCH] fix: Add modified version of RollingFileLogWriter --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 17 +- .../yokai/core/RollingUniFileLogWriter.kt | 160 ++++++++++++++++++ .../yokai/domain/storage/StorageManager.kt | 28 +-- .../tachiyomi/util/system/KermitExtensions.kt | 17 +- gradle/libs.versions.toml | 3 +- 5 files changed, 175 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/yokai/core/RollingUniFileLogWriter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 1d9820ed8a..6d26074d33 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -56,21 +56,19 @@ import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.localeContext -import eu.kanade.tachiyomi.util.system.nameWithoutExtension import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.setToDefault -import eu.kanade.tachiyomi.util.system.setupFileLog import java.security.Security import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.io.files.Path import org.conscrypt.Conscrypt import org.koin.core.context.startKoin import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.core.CrashlyticsLogWriter +import yokai.core.RollingUniFileLogWriter import yokai.core.di.appModule import yokai.core.di.domainModule import yokai.core.di.preferenceModule @@ -113,7 +111,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F val scope = ProcessLifecycleOwner.get().lifecycleScope - Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsFile())) + Logger.setToDefault(buildLogWritersToAdd(storageManager.getLogsDirectory())) basePreferences.crashReport().changes() .onEach { @@ -284,18 +282,11 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F } fun buildLogWritersToAdd( - logFile: UniFile?, + logPath: UniFile?, ) = buildList { if (!BuildConfig.DEBUG) add(CrashlyticsLogWriter()) - //val fileName = logFile?.nameWithoutExtension - //val filePath = logFile?.parentFile?.filePath?.let { path -> Path(path) } - //if (filePath != null && fileName != null) add( - // Logger.setupFileLog( - // logFileName = fileName, - // logPath = filePath, - // ) - //) + if (logPath != null) add(RollingUniFileLogWriter(logPath)) } private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" diff --git a/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt new file mode 100644 index 0000000000..c2c7dc7898 --- /dev/null +++ b/app/src/main/java/yokai/core/RollingUniFileLogWriter.kt @@ -0,0 +1,160 @@ +package yokai.core + +import co.touchlab.kermit.DefaultFormatter +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Message +import co.touchlab.kermit.MessageStringFormatter +import co.touchlab.kermit.Severity +import co.touchlab.kermit.Tag +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.withIOContext +import java.io.IOException +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive +import kotlinx.coroutines.newSingleThreadContext + +/** + * Copyright (c) 2024 Touchlab + * SPDX-License-Identifier: Apache-2.0 + * + * Kermit's RollingFileLogWriter, modified to use UniFile since using KotlinX IO's FileSystem keep throwing + * "Permission Denied". Also added try-catch, in case "Permission Denied" is back to haunt me + * + * REF: https://github.com/touchlab/Kermit/blob/c9af0b7d3344b430f4ed2668e74d02f34ba1905a/kermit-io/src/commonMain/kotlin/co/touchlab/kermit/io/RollingFileLogWriter.kt + */ +class RollingUniFileLogWriter( + private val logPath: UniFile, + private val rollOnSize: Long = 10 * 1024 * 1024, // 10MB + private val maxLogFiles: Int = 5, + private val messageStringFormatter: MessageStringFormatter = DefaultFormatter, + private val messageDateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) +) : LogWriter() { + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + private val coroutineScope = CoroutineScope( + newSingleThreadContext("RollingUniFileLogWriter") + + SupervisorJob() + + CoroutineName("RollingUniFileLogWriter") + + CoroutineExceptionHandler { _, throwable -> + println("RollingUniFileLogWriter: Uncaught exception in writer coroutine") + throwable.printStackTrace() + } + ) + + private val loggingChannel: Channel = Channel() + + init { + coroutineScope.launchIO { + writer() + } + } + + override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { + bufferLog( + formatMessage( + severity = severity, + tag = Tag(tag), + message = Message(message), + ), throwable + ) + } + + private fun bufferLog(message: String, throwable: Throwable?) { + val log = buildString { + append(messageDateFormat.format(Date())) + append(" ") + appendLine(message) + if (throwable != null) { + appendLine(throwable.stackTraceToString()) + } + } + loggingChannel.trySendBlocking(log.toByteArray()) + } + + private fun formatMessage(severity: Severity, tag: Tag?, message: Message): String = + messageStringFormatter.formatMessage(severity, tag, message) + + private fun maybeRollLogs(size: Long): Boolean { + return if (size > rollOnSize) { + rollLogs() + true + } else false + } + + private fun rollLogs() { + if (pathForLogIndex(maxLogFiles - 1)?.exists() == true) { + pathForLogIndex(maxLogFiles - 1)?.delete() + } + + (0..<(maxLogFiles - 1)).reversed().forEach { + val sourcePath = pathForLogIndex(it) + val targetFileName = fileNameForLogIndex(it + 1) + if (sourcePath?.exists() == true) { + try { + sourcePath.renameTo(targetFileName) + } catch (e: Exception) { + println("RollingUniFileLogWriter: Failed to roll log file ${sourcePath.filePath} to $targetFileName (sourcePath exists=${sourcePath.exists()})") + e.printStackTrace() + } + } + } + } + + private fun fileNameForLogIndex(index: Int): String { + val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + return if (index == 0) "${date}-${BuildConfig.BUILD_TYPE}.log" else "${date}-${BuildConfig.BUILD_TYPE} (${index}).log" + } + + private fun pathForLogIndex(index: Int, create: Boolean = false): UniFile? { + return if (create) logPath.createFile(fileNameForLogIndex(index)) else logPath.findFile(fileNameForLogIndex(index)) + } + + private suspend fun writer() = withIOContext { + val logFilePath = pathForLogIndex(0) + + if (logFilePath?.exists() == true) { + maybeRollLogs(fileSize(logFilePath)) + } + + fun openNewOutput() = pathForLogIndex(0, true)?.openOutputStream(true) + + var currentLogSink = openNewOutput() + + while (currentCoroutineContext().isActive) { + val result = loggingChannel.receiveCatching() + + val rolled = maybeRollLogs(fileSize(logFilePath)) + if (rolled) { + currentLogSink?.close() + currentLogSink = openNewOutput() + } + + result.getOrNull()?.let { + try { + currentLogSink?.write(it) + } catch (e: IOException) { + // Probably "Permission Denied" is back to haunt me + println("RollingUniFileLogWriter: Failed to write to log file") + e.printStackTrace() + } + } + + currentLogSink?.flush() + } + } + + private fun fileSize(path: UniFile?) = path?.length() ?: -1L +} diff --git a/app/src/main/java/yokai/domain/storage/StorageManager.kt b/app/src/main/java/yokai/domain/storage/StorageManager.kt index 4ab37dc12e..372dc718e6 100644 --- a/app/src/main/java/yokai/domain/storage/StorageManager.kt +++ b/app/src/main/java/yokai/domain/storage/StorageManager.kt @@ -4,13 +4,9 @@ import android.content.Context import androidx.core.net.toUri import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile -import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.buildLogWritersToAdd import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.setToDefault -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -21,7 +17,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.shareIn -import kotlinx.io.files.Path class StorageManager( private val context: Context, @@ -50,15 +45,15 @@ class StorageManager( } parent.createDirectory(COVERS_PATH) parent.createDirectory(PAGES_PATH) - parent.createDirectory(LOGS_PATH) - } - - try { - Logger.setToDefault(buildLogWritersToAdd(getLogsFile())) - } catch (e: Exception) { - // Just in case something went horribly wrong - Logger.setToDefault(buildLogWritersToAdd(null)) - Logger.e(e) { "Something went wrong while trying to setup log file" } + parent.createDirectory(LOGS_PATH)?.also { + try { + Logger.setToDefault(buildLogWritersToAdd(it)) + } catch (e: Exception) { + // Just in case something went horribly wrong + Logger.setToDefault(buildLogWritersToAdd(null)) + Logger.e(e) { "Something went wrong while trying to setup log file" } + } + } } _changes.send(Unit) @@ -98,11 +93,6 @@ class StorageManager( fun getLogsDirectory(): UniFile? { return baseDir?.createDirectory(LOGS_PATH) } - - fun getLogsFile(): UniFile? { - val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) - return getLogsDirectory()?.createFile("${date}-${BuildConfig.BUILD_TYPE}.log") - } } private const val BACKUPS_PATH = "backup" diff --git a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt index dffc72481f..e27d63e46d 100644 --- a/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt +++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/util/system/KermitExtensions.kt @@ -2,27 +2,12 @@ package eu.kanade.tachiyomi.util.system import co.touchlab.kermit.LogWriter import co.touchlab.kermit.Logger -import co.touchlab.kermit.io.RollingFileLogWriter -import co.touchlab.kermit.io.RollingFileLogWriterConfig import co.touchlab.kermit.platformLogWriter -import kotlinx.io.files.Path fun Logger.w(e: Throwable) = w(e) { "Something is not right..." } fun Logger.e(e: Throwable) = e(e) { "Something went wrong!" } -fun Logger.setToDefault( - writersToAdd: List, -) { +fun Logger.setToDefault(writersToAdd: List) { Logger.setLogWriters(listOf(platformLogWriter()) + writersToAdd) Logger.setTag("Yokai") } - -fun Logger.setupFileLog(logFileName: String, logPath: Path): LogWriter { - return RollingFileLogWriter( - config = RollingFileLogWriterConfig( - logFileName = logFileName, - logFilePath = logPath, - maxLogFiles = 1, - ) - ) -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 973b6c41f6..1469060e7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,6 @@ flexible-adapter = { module = "com.github.arkon.FlexibleAdapter:flexible-adapter image-decoder = { module = "com.github.tachiyomiorg:image-decoder", version = "41c059e540" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } -kermit-io = { module = "co.touchlab:kermit-io", version.ref = "kermit" } koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core" } @@ -114,7 +113,7 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } db = [ "sqldelight-coroutines" ] db-android = [ "sqldelight-android-driver", "sqldelight-android-paging" ] coil = [ "coil3", "coil3-svg", "coil3-gif", "coil3-okhttp" ] -logging = [ "kermit", "kermit-io" ] +logging = [ "kermit" ] # FIXME: Uncomment once SQLDelight support KMP AndroidX SQLiteDriver #sqlite = [ "sqlite", "sqlite-ktx" ] sqlite = [ "sqlite-ktx" ]