From 5ac74a336f0dfaf60c663e1da09debbf13dbd9b6 Mon Sep 17 00:00:00 2001 From: Karlatemp Date: Wed, 31 Aug 2022 22:40:27 +0800 Subject: [PATCH] Create module `mirai-console-frontend-base` --- .../build.gradle.kts | 38 +++++ .../compatibility-validation/jvm/api/jvm.api | 83 ++++++++++ ...tractMiraiConsoleFrontendImplementation.kt | 121 ++++++++++++++ .../src/FrontendBase.kt | 142 ++++++++++++++++ .../src/RepipedMessageForward.kt | 115 +++++++++++++ .../src/logging/LogRecorder.kt | 156 ++++++++++++++++++ .../src/package.kt | 10 ++ .../test/RepipedMessageForwardTest.kt | 112 +++++++++++++ .../test/package.kt | 10 ++ .../mirai-console-terminal/build.gradle.kts | 1 + .../jvmBaseMain/kotlin/utils/MiraiLogger.kt | 12 +- settings.gradle.kts | 1 + 12 files changed, 799 insertions(+), 2 deletions(-) create mode 100644 mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts create mode 100644 mirai-console/frontend/mirai-console-frontend-base/compatibility-validation/jvm/api/jvm.api create mode 100644 mirai-console/frontend/mirai-console-frontend-base/src/AbstractMiraiConsoleFrontendImplementation.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/src/FrontendBase.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/src/RepipedMessageForward.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/src/logging/LogRecorder.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/src/package.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/test/RepipedMessageForwardTest.kt create mode 100644 mirai-console/frontend/mirai-console-frontend-base/test/package.kt diff --git a/mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts b/mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts new file mode 100644 index 000000000..7163d8213 --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +import BinaryCompatibilityConfigurator.configureBinaryValidator + +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("java") + `maven-publish` +} + +kotlin { + explicitApiWarning() +} + +dependencies { + compileAndTestRuntime(project(":mirai-core-utils")) + + compileAndTestRuntime(project(":mirai-console")) + compileAndTestRuntime(project(":mirai-core-api")) + compileAndTestRuntime(project(":mirai-core-utils")) + compileAndTestRuntime(`kotlin-stdlib-jdk8`) +} + +version = Versions.consoleTerminal + +description = "Console frontend abstract" + +configurePublishing("mirai-console-frontend-base") +configureBinaryValidator(null) + diff --git a/mirai-console/frontend/mirai-console-frontend-base/compatibility-validation/jvm/api/jvm.api b/mirai-console/frontend/mirai-console-frontend-base/compatibility-validation/jvm/api/jvm.api new file mode 100644 index 000000000..fe24be49d --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/compatibility-validation/jvm/api/jvm.api @@ -0,0 +1,83 @@ +public abstract class net/mamoe/mirai/console/frontendbase/AbstractMiraiConsoleFrontendImplementation : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/console/MiraiConsoleImplementation { + public fun (Ljava/lang/String;)V + public fun createLogger (Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger; + public fun createLoggerFactory (Lnet/mamoe/mirai/console/MiraiConsoleImplementation$FrontendLoggingInitContext;)Lnet/mamoe/mirai/utils/MiraiLogger$Factory; + public fun getBuiltInPluginLoaders ()Ljava/util/List; + public fun getCommandManager ()Lnet/mamoe/mirai/console/command/CommandManager; + public fun getConfigStorageForBuiltIns ()Lnet/mamoe/mirai/console/data/PluginDataStorage; + public fun getConfigStorageForJvmPluginLoader ()Lnet/mamoe/mirai/console/data/PluginDataStorage; + public fun getConsoleDataScope ()Lnet/mamoe/mirai/console/MiraiConsoleImplementation$ConsoleDataScope; + public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext; + public fun getDataStorageForBuiltIns ()Lnet/mamoe/mirai/console/data/PluginDataStorage; + public fun getDataStorageForJvmPluginLoader ()Lnet/mamoe/mirai/console/data/PluginDataStorage; + protected abstract fun getFrontendBase ()Lnet/mamoe/mirai/console/frontendbase/FrontendBase; + public fun getJvmPluginLoader ()Lnet/mamoe/mirai/console/plugin/jvm/JvmPluginLoader; +} + +public abstract class net/mamoe/mirai/console/frontendbase/FrontendBase { + public fun ()V + public fun getDaemonThreadGroup ()Ljava/lang/ThreadGroup; + public fun getLogDropAnsi ()Z + public fun getLoggingDirectory ()Ljava/nio/file/Path; + public fun getLoggingRecorder ()Lnet/mamoe/mirai/console/frontendbase/logging/LogRecorder; + public abstract fun getScope ()Lkotlinx/coroutines/CoroutineScope; + public fun getThreadGroup ()Ljava/lang/ThreadGroup; + public abstract fun getWorkingDirectory ()Ljava/nio/file/Path; + protected fun initLogRecorder ()Lnet/mamoe/mirai/console/frontendbase/logging/LogRecorder; + protected fun initScreen_forwardStdToMiraiLogger ()V + protected fun initScreen_forwardStdToScreen ()V + public fun newDaemon (Ljava/lang/String;Ljava/lang/Runnable;)Ljava/lang/Thread; + public fun newThread (Ljava/lang/String;Ljava/lang/Runnable;)Ljava/lang/Thread; + public fun newThreadFactory (Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Ljava/util/concurrent/ThreadFactory; + public static synthetic fun newThreadFactory$default (Lnet/mamoe/mirai/console/frontendbase/FrontendBase;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/util/concurrent/ThreadFactory; + public abstract fun printToScreenDirectly (Ljava/lang/String;)V + public fun recordToLogging (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/console/frontendbase/logging/AllDroppedLogRecorder : net/mamoe/mirai/console/frontendbase/logging/LogRecorder { + public static final field INSTANCE Lnet/mamoe/mirai/console/frontendbase/logging/AllDroppedLogRecorder; + public fun record (Ljava/lang/String;)V +} + +public abstract class net/mamoe/mirai/console/frontendbase/logging/AsyncLogRecorder : net/mamoe/mirai/console/frontendbase/logging/LogRecorder { + public fun (Lnet/mamoe/mirai/console/frontendbase/FrontendBase;I)V + public synthetic fun (Lnet/mamoe/mirai/console/frontendbase/FrontendBase;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected abstract fun asyncRecord (Ljava/lang/String;)V + protected final fun getChannel ()Lkotlinx/coroutines/channels/Channel; + protected final fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; + protected final fun getSubscope ()Lkotlinx/coroutines/CoroutineScope; + protected final fun getThreadPool ()Ljava/util/concurrent/ScheduledExecutorService; + public fun record (Ljava/lang/String;)V +} + +public class net/mamoe/mirai/console/frontendbase/logging/AsyncLogRecorderForwarded : net/mamoe/mirai/console/frontendbase/logging/AsyncLogRecorder { + public fun (Lnet/mamoe/mirai/console/frontendbase/logging/LogRecorder;Lnet/mamoe/mirai/console/frontendbase/FrontendBase;I)V + public synthetic fun (Lnet/mamoe/mirai/console/frontendbase/logging/LogRecorder;Lnet/mamoe/mirai/console/frontendbase/FrontendBase;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected fun asyncRecord (Ljava/lang/String;)V + protected final fun getDelegate ()Lnet/mamoe/mirai/console/frontendbase/logging/LogRecorder; +} + +public class net/mamoe/mirai/console/frontendbase/logging/DailySplitLogRecorder : net/mamoe/mirai/console/frontendbase/logging/LogRecorder { + protected field lastDate I + protected field writer Ljava/io/Writer; + public fun (Ljava/nio/file/Path;Lnet/mamoe/mirai/console/frontendbase/FrontendBase;Ljava/time/format/DateTimeFormatter;)V + public synthetic fun (Ljava/nio/file/Path;Lnet/mamoe/mirai/console/frontendbase/FrontendBase;Ljava/time/format/DateTimeFormatter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + protected final fun acquireFileWriter ()V + protected final fun getBase ()Lnet/mamoe/mirai/console/frontendbase/FrontendBase; + protected final fun getDateFormatter ()Ljava/time/format/DateTimeFormatter; + protected final fun getDirectory ()Ljava/nio/file/Path; + public fun record (Ljava/lang/String;)V +} + +public abstract class net/mamoe/mirai/console/frontendbase/logging/LogRecorder { + public fun ()V + public abstract fun record (Ljava/lang/String;)V +} + +public class net/mamoe/mirai/console/frontendbase/logging/WriterLogRecorder : net/mamoe/mirai/console/frontendbase/logging/LogRecorder { + public fun (Ljava/io/Writer;Lnet/mamoe/mirai/console/frontendbase/FrontendBase;)V + protected final fun getBase ()Lnet/mamoe/mirai/console/frontendbase/FrontendBase; + protected final fun getWriter ()Ljava/io/Writer; + public fun record (Ljava/lang/String;)V +} + diff --git a/mirai-console/frontend/mirai-console-frontend-base/src/AbstractMiraiConsoleFrontendImplementation.kt b/mirai-console/frontend/mirai-console-frontend-base/src/AbstractMiraiConsoleFrontendImplementation.kt new file mode 100644 index 000000000..7d158c197 --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/src/AbstractMiraiConsoleFrontendImplementation.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase + +import kotlinx.coroutines.* +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.MiraiConsoleImplementation +import net.mamoe.mirai.console.command.CommandManager +import net.mamoe.mirai.console.data.MultiFilePluginDataStorage +import net.mamoe.mirai.console.data.PluginDataStorage +import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader +import net.mamoe.mirai.console.plugin.loader.PluginLoader +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.PlatformLogger +import kotlin.coroutines.CoroutineContext + +/** + * [MiraiConsoleImplementation] 的基本抽象实现 + * + * @param frontendCoroutineName 该前端的名字, 如 `"MiraiConsoleImplementationTerminal"` + * @see FrontendBase + */ +public abstract class AbstractMiraiConsoleFrontendImplementation( + frontendCoroutineName: String, +) : MiraiConsoleImplementation, CoroutineScope { + + // region 此 region 的 字段 / 方法 为 console 默认/内部 实现, 如无必要不建议修改 + private val delegateCoroutineScope by lazy { + CoroutineScope( + SupervisorJob() + + CoroutineName(frontendCoroutineName) + + CoroutineExceptionHandler { coroutineContext, throwable -> + if (throwable is CancellationException) { + return@CoroutineExceptionHandler + } + val coroutineName = coroutineContext[CoroutineName]?.name ?: "" + MiraiConsole.mainLogger.error("Exception in coroutine $coroutineName", throwable) + } + ) + } + override val coroutineContext: CoroutineContext get() = delegateCoroutineScope.coroutineContext + + override val builtInPluginLoaders: List>> = listOf(lazy { JvmPluginLoader }) + override val jvmPluginLoader: JvmPluginLoader by lazy { backendAccess.createDefaultJvmPluginLoader(coroutineContext) } + override val commandManager: CommandManager by lazy { backendAccess.createDefaultCommandManager(coroutineContext) } + override val consoleDataScope: MiraiConsoleImplementation.ConsoleDataScope by lazy { + MiraiConsoleImplementation.ConsoleDataScope.createDefault( + coroutineContext, dataStorageForBuiltIns, configStorageForBuiltIns + ) + } + override val dataStorageForJvmPluginLoader: PluginDataStorage by lazy { + MultiFilePluginDataStorage(rootPath.resolve("data")) + } + override val dataStorageForBuiltIns: PluginDataStorage by lazy { + MultiFilePluginDataStorage(rootPath.resolve("data")) + } + override val configStorageForJvmPluginLoader: PluginDataStorage by lazy { + MultiFilePluginDataStorage(rootPath.resolve("config")) + } + override val configStorageForBuiltIns: PluginDataStorage by lazy { + MultiFilePluginDataStorage(rootPath.resolve("config")) + } + // endregion + + // region + protected abstract val frontendBase: FrontendBase + // endregion + + + // region Logging + @Deprecated( + "Deprecated for removal. Implement the other overload, or use MiraiConsole.createLogger instead.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + "MiraiLogger.Factory.create(javaClass, identity)", + "net.mamoe.mirai.utils.MiraiLogger" + ) + ) + override fun createLogger(identity: String?): MiraiLogger { + return MiraiLogger.Factory.create(javaClass, identity) + } + + override fun createLoggerFactory(context: MiraiConsoleImplementation.FrontendLoggingInitContext): MiraiLogger.Factory { + @Suppress("INVISIBLE_MEMBER") + frontendBase.initScreen_forwardStdToScreen() + + // region Default Fallback Implementation + + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + net.mamoe.mirai.utils.MiraiLoggerFactoryImplementationBridge.defaultLoggerFactory = { + + class DefaultMiraiConsoleFactory : MiraiLogger.Factory { + // Don't directly use ::println + // ::println will query System.out every time. + private val stdout: ((String) -> Unit) = System.out::println + + override fun create(requester: Class<*>, identity: String?): MiraiLogger { + return PlatformLogger(identity ?: requester.kotlin.simpleName ?: requester.simpleName, stdout) + } + } + + DefaultMiraiConsoleFactory() + } + // endregion + + val factoryImpl = context.acquirePlatformImplementation() + context.invokeAfterInitialization { + @Suppress("INVISIBLE_MEMBER") + frontendBase.initScreen_forwardStdToMiraiLogger() + } + return factoryImpl + } + // endregion +} \ No newline at end of file diff --git a/mirai-console/frontend/mirai-console-frontend-base/src/FrontendBase.kt b/mirai-console/frontend/mirai-console-frontend-base/src/FrontendBase.kt new file mode 100644 index 000000000..3cca03bad --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/src/FrontendBase.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase + +import kotlinx.coroutines.CoroutineScope +import net.mamoe.mirai.console.MiraiConsoleImplementation +import net.mamoe.mirai.console.frontendbase.logging.AsyncLogRecorderForwarded +import net.mamoe.mirai.console.frontendbase.logging.DailySplitLogRecorder +import net.mamoe.mirai.console.frontendbase.logging.LogRecorder +import net.mamoe.mirai.utils.MiraiLogger +import java.io.PrintStream +import java.nio.file.Path +import java.util.concurrent.ThreadFactory +import java.util.concurrent.atomic.AtomicInteger + + +/** + * 前端的基本实现 + * + * @see AbstractMiraiConsoleFrontendImplementation + */ +public abstract class FrontendBase { + /** + * 所属前端的 [CoroutineScope] + * + * Implementation note: 直接返回前端实例 + */ + public abstract val scope: CoroutineScope + + public open val threadGroup: ThreadGroup by lazy { + ThreadGroup("Mirai Console FrontEnd Threads") + } + public open val daemonThreadGroup: ThreadGroup by lazy { + ThreadGroup(threadGroup, "Mirai Console FrontEnd Daemon Threads") + } + public open val loggingRecorder: LogRecorder by lazy { initLogRecorder() } + + /** + * Console 的运行目录 + * + * Implementation note: 返回 [MiraiConsoleImplementation.rootPath] + */ + public abstract val workingDirectory: Path + + /** + * 日志存放目录 + */ + public open val loggingDirectory: Path by lazy { workingDirectory.resolve("logs") } + + /** + * 存储的日志是否需要去除 ansi 标志 + */ + public open val logDropAnsi: Boolean get() = true + + /** + * 创建一个新的非守护线程, 此线程不会预先启动 + */ + public open fun newThread(name: String, task: Runnable): Thread { + return Thread(threadGroup, task, name) + } + + /** + * 创建一个新的守护线程, 此线程不会预先启动 + */ + public open fun newDaemon(name: String, task: Runnable): Thread { + return Thread(daemonThreadGroup, task, name).apply { + isDaemon = true + } + } + + /** + * 创建一个新的 [ThreadFactory], 创建的线程的名字为 `$name#{counter}` + */ + public open fun newThreadFactory(name: String, isDemon: Boolean, postSetup: (Thread) -> Unit = {}): ThreadFactory { + return object : ThreadFactory { + private val group = ThreadGroup(if (isDemon) daemonThreadGroup else threadGroup, name) + private val counter = AtomicInteger() + + override fun newThread(r: Runnable): Thread { + return Thread(group, r, "$name#${counter.getAndIncrement()}").also { + it.isDaemon = isDemon + }.also(postSetup) + } + } + } + + /** + * 将一条信息直接打印到前端的屏幕上 + * + * Implementation note: 打印时不需要添加任何修饰, [msg] 已经是格式化好的信息 + */ + public abstract fun printToScreenDirectly(msg: String) + + @Suppress("FunctionName") + protected open fun initScreen_forwardStdToScreen() { + val forwarder = RepipedMessageForward { msg -> + printToScreenDirectly(msg) + recordToLogging(msg) + } + val printer = PrintStream(forwarder.pipedOutputStream, true, "UTF-8") + System.setOut(printer) + + // stderr is reserved for printing fatal errors when something crashed in logger factory initialization + } + + @Suppress("FunctionName") + protected open fun initScreen_forwardStdToMiraiLogger() { + val logStdout = MiraiLogger.Factory.create(javaClass, "stdout") + val logStderr = MiraiLogger.Factory.create(javaClass, "stderr") + + val forwarderStdout = RepipedMessageForward(logStdout::info) + val forwarderStderr = RepipedMessageForward(logStderr::warning) + + + System.setOut(PrintStream(forwarderStdout.pipedOutputStream, true, "UTF-8")) + System.setErr(PrintStream(forwarderStderr.pipedOutputStream, true, "UTF-8")) + } + + /** + * 将一条消息记录至日志 (不会显示至屏幕) + */ + public open fun recordToLogging(msg: String) { + loggingRecorder.record(msg) + } + + protected open fun initLogRecorder(): LogRecorder { + return AsyncLogRecorderForwarded( + DailySplitLogRecorder( + loggingDirectory, + this + ), + this + ) + } +} diff --git a/mirai-console/frontend/mirai-console-frontend-base/src/RepipedMessageForward.kt b/mirai-console/frontend/mirai-console-frontend-base/src/RepipedMessageForward.kt new file mode 100644 index 000000000..058f6f807 --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/src/RepipedMessageForward.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase + +import java.io.ByteArrayOutputStream +import java.io.OutputStream + +internal class RepipedMessageForward( + private val output: (String) -> Unit, +) : ByteArrayOutputStream(1024 * 1024) { + internal val pipedOutputStream: OutputStream get() = this + + + private var lastCheckIndex = 0 + + @Synchronized + override fun write(b: ByteArray?, off: Int, len: Int) { + super.write(b, off, len) + flush() + + } + + @Synchronized + override fun write(b: Int) { + super.write(b) + flush() + } + + @Synchronized + override fun write(b: ByteArray?) { + super.write(b) + flush() + } + + @Synchronized + override fun flush() { + topLoop@ + while (true) { + + var index = lastCheckIndex + val end = this.count + var lastIsLr = false // last char is '\r' + while (index < end) { + val c = buf[index].toInt() and 0xFF + when (c shr 4) { + in 0..7 -> { + /* 0xxxxxxx*/ + + if (c == '\r'.code) { + lastIsLr = true + } else if (c == 10) { + // NEW LINE: \n + val strend = if (lastIsLr) { + index - 1 + } else { + index + } + val strx = String(buf, 0, strend, Charsets.UTF_8) + + + index++ + System.arraycopy( + buf, index, buf, 0, end - index + ) + + // A \n + + // index = 1 + // string with ln = 2 + + count -= index + lastCheckIndex = 0 + output(strx) + + continue@topLoop // same as return flush() + + } else { + lastIsLr = false + } + + index++ + } + 12, 13 -> { + /* 110x xxxx 10xx xxxx*/ + index += 2 + lastIsLr = false + } + 14 -> { + /* 1110 xxxx 10xx xxxx 10xx xxxx */ + index += 3 + lastIsLr = false + } + else -> { + /* 10xx xxxx, 1111 xxxx */ + index++// Ignored + lastIsLr = false + } + } + + } + lastCheckIndex = index + + break + } + } + +} + diff --git a/mirai-console/frontend/mirai-console-frontend-base/src/logging/LogRecorder.kt b/mirai-console/frontend/mirai-console-frontend-base/src/logging/LogRecorder.kt new file mode 100644 index 000000000..657c13e3d --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/src/logging/LogRecorder.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +@file:Suppress("MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.console.frontendbase.logging + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.onFailure +import net.mamoe.mirai.console.frontendbase.FrontendBase +import net.mamoe.mirai.console.util.AnsiMessageBuilder.Companion.dropAnsi +import net.mamoe.mirai.utils.childScope +import java.io.Writer +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService + +public abstract class LogRecorder { + public abstract fun record(msg: String) +} + +public object AllDroppedLogRecorder : LogRecorder() { + override fun record(msg: String) { + } +} + +public abstract class AsyncLogRecorder( + private val base: FrontendBase, + pipelineSize: Int = 2048, +) : LogRecorder() { + protected val channel: Channel = Channel(pipelineSize) + protected val threadPool: ScheduledExecutorService = Executors.newScheduledThreadPool( + 3, + base.newThreadFactory("Mirai Console Logging", true) + ) + protected val dispatcher: CoroutineDispatcher = threadPool.asCoroutineDispatcher() + + protected val subscope: CoroutineScope = base.scope.childScope(name = "Mirai Console Async Logging", dispatcher) + + init { + base.scope.coroutineContext.job.invokeOnCompletion { + channel.close() + threadPool.shutdown() + } + + subscope.launch { + while (isActive) { + val nextLine = channel.receive() + asyncRecord(nextLine) + } + } + } + + override fun record(msg: String) { + if (!subscope.isActive) return // Died + + channel.trySend(msg).onFailure { + base.scope.launch(start = CoroutineStart.UNDISPATCHED) { + channel.send(msg) + } + } + } + + protected abstract fun asyncRecord(msg: String) +} + +public open class AsyncLogRecorderForwarded( + protected val delegate: LogRecorder, + base: FrontendBase, + pipelineSize: Int = 2048, +) : AsyncLogRecorder(base, pipelineSize) { + override fun asyncRecord(msg: String) { + delegate.record(msg) + } +} + +public open class WriterLogRecorder( + protected val writer: Writer, + protected val base: FrontendBase, +) : LogRecorder() { + override fun record(msg: String) { + try { + writer.append( + if (base.logDropAnsi) { + msg.dropAnsi() + } else msg + ).append('\n').flush() + } catch (e: Throwable) { + base.printToScreenDirectly(e.stackTraceToString()) + } + } +} + +public open class DailySplitLogRecorder( + protected val directory: Path, + protected val base: FrontendBase, + protected val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern( + "YYYY-MM-dd'.log'" + ), +) : LogRecorder() { + @JvmField + protected var writer: Writer? = null + + @JvmField + protected var lastDate: Int = -1 + + protected fun acquireFileWriter() { + val instantNow = Instant.now() + .atZone(ZoneId.systemDefault()) + + val dayNow = instantNow.dayOfYear + + if (dayNow != lastDate) { + lastDate = dayNow + + writer?.close() + + val logPath = directory.resolve(dateFormatter.format(instantNow)) + logPath.parent?.let { pt -> + if (!Files.isDirectory(pt)) { + Files.createDirectories(pt) + } + } + + writer = Files.newBufferedWriter( + logPath, Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND + ) + } + } + + override fun record(msg: String) { + try { + acquireFileWriter() + + (writer ?: error("Writer not setup")).append( + if (base.logDropAnsi) { + msg.dropAnsi() + } else msg + ).append('\n').flush() + } catch (e: Throwable) { + base.printToScreenDirectly(e.stackTraceToString()) + } + } +} diff --git a/mirai-console/frontend/mirai-console-frontend-base/src/package.kt b/mirai-console/frontend/mirai-console-frontend-base/src/package.kt new file mode 100644 index 000000000..cc2f96259 --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/src/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase diff --git a/mirai-console/frontend/mirai-console-frontend-base/test/RepipedMessageForwardTest.kt b/mirai-console/frontend/mirai-console-frontend-base/test/RepipedMessageForwardTest.kt new file mode 100644 index 000000000..c47382bba --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/test/RepipedMessageForwardTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase + +import java.io.OutputStreamWriter +import java.io.PrintStream +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class RepipedMessageForwardTest { + private val pendingMsg = mutableListOf() + private val ouptut = RepipedMessageForward(pendingMsg::add).pipedOutputStream + + @Test + fun testPrintStream() { + val ps = PrintStream(ouptut) + ps.println("ABC") + ps.append("D").append("E").append("F").println("G") + ps.println("LLOO") + + assertEquals(3, pendingMsg.size) + assertEquals("ABC", pendingMsg.removeAt(0)) + assertEquals("DEFG", pendingMsg.removeAt(0)) + assertEquals("LLOO", pendingMsg.removeAt(0)) + } + + @Test + fun testCRLF() { + OutputStreamWriter(ouptut).use { writer -> + + writer.append("LINE").append("AAA").append("OOOO").append("\r\n") + writer.append("Line1125744\r\n") + writer.append("AFFXZ\r\n") + + } + + + assertEquals(3, pendingMsg.size) + assertEquals("LINEAAAOOOO", pendingMsg.removeAt(0)) + assertEquals("Line1125744", pendingMsg.removeAt(0)) + assertEquals("AFFXZ", pendingMsg.removeAt(0)) + } + + @Test + fun testLF() { + + OutputStreamWriter(ouptut).use { writer -> + + writer.append("LINE").append("\n") + writer.append("Line5\n") + writer.append("AFFXZ\n") + writer.append("NO\rCR REMOVED\n") + + } + + + assertEquals(4, pendingMsg.size) + assertEquals("LINE", pendingMsg.removeAt(0)) + assertEquals("Line5", pendingMsg.removeAt(0)) + assertEquals("AFFXZ", pendingMsg.removeAt(0)) + assertEquals("NO\rCR REMOVED", pendingMsg.removeAt(0)) + } + + @Test + fun testCRLFMixing() { + + OutputStreamWriter(ouptut).use { writer -> + writer.append("LF\n") + writer.append("CRLF\r\n") + writer.append("LFLF\n\n") + } + + assertEquals(4, pendingMsg.size) + assertEquals("LF", pendingMsg.removeAt(0)) + assertEquals("CRLF", pendingMsg.removeAt(0)) + assertEquals("LFLF", pendingMsg.removeAt(0)) + assertEquals("", pendingMsg.removeAt(0)) + } + + @Test + fun testEmptyLines_LF() { + OutputStreamWriter(ouptut).use { writer -> + repeat(7) { + writer.append("\n") + } + } + assertEquals(7, pendingMsg.size) + repeat(7) { + assertEquals("", pendingMsg.removeAt(0)) + } + } + + @Test + fun testEmptyLines_CRLF() { + OutputStreamWriter(ouptut).use { writer -> + repeat(7) { + writer.append("\r\n") + } + } + assertEquals(7, pendingMsg.size) + repeat(7) { + assertEquals("", pendingMsg.removeAt(0)) + } + } +} diff --git a/mirai-console/frontend/mirai-console-frontend-base/test/package.kt b/mirai-console/frontend/mirai-console-frontend-base/test/package.kt new file mode 100644 index 000000000..cc2f96259 --- /dev/null +++ b/mirai-console/frontend/mirai-console-frontend-base/test/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.frontendbase diff --git a/mirai-console/frontend/mirai-console-terminal/build.gradle.kts b/mirai-console/frontend/mirai-console-terminal/build.gradle.kts index 75555a3dc..0c02dd03c 100644 --- a/mirai-console/frontend/mirai-console-terminal/build.gradle.kts +++ b/mirai-console/frontend/mirai-console-terminal/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { compileAndTestRuntime(project(":mirai-console")) compileAndTestRuntime(project(":mirai-core-api")) compileAndTestRuntime(project(":mirai-core-utils")) + compileAndTestRuntime(project(":mirai-console-frontend-base")) compileAndTestRuntime(kotlin("stdlib-jdk8", Versions.kotlinStdlib)) // must specify `compileOnly` explicitly testApi(project(":mirai-core")) diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt index f7077c8b4..4197c6fbe 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt +++ b/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt @@ -266,11 +266,18 @@ public actual interface MiraiLogger { */ internal object MiraiLoggerFactoryImplementationBridge : MiraiLogger.Factory { @Suppress("ObjectPropertyName") - private var _instance by lateinitMutableProperty { createPlatformInstance() } + private var _instance by lateinitMutableProperty { + createPlatformInstance() + } internal val instance get() = _instance - fun createPlatformInstance() = loadService(MiraiLogger.Factory::class) { DefaultFactory() } + // It is required for MiraiConsole because default implementation + // queries stdout on every message printing + // It creates an infinite loop (StackOverflowError) + internal var defaultLoggerFactory: (() -> MiraiLogger.Factory) = ::DefaultFactory + + fun createPlatformInstance() = loadService(MiraiLogger.Factory::class, defaultLoggerFactory) private val frozen = atomic(false) @@ -280,6 +287,7 @@ internal object MiraiLoggerFactoryImplementationBridge : MiraiLogger.Factory { @TestOnly fun reinit() { + defaultLoggerFactory = ::DefaultFactory frozen.loop { value -> _instance = createPlatformInstance() if (frozen.compareAndSet(value, false)) return diff --git a/settings.gradle.kts b/settings.gradle.kts index a1cc6c0f8..c3e877ef5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -77,6 +77,7 @@ includeConsoleProject(":mirai-console-compiler-annotations", "tools/compiler-ann if (getLocalProperty("projects.mirai-console.enabled", true)) { includeConsoleProject(":mirai-console", "backend/mirai-console") includeConsoleProject(":mirai-console.codegen", "backend/codegen") + includeConsoleProject(":mirai-console-frontend-base", "frontend/mirai-console-frontend-base") includeConsoleProject(":mirai-console-terminal", "frontend/mirai-console-terminal") includeConsoleIntegrationTestProjects()