diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index f19f40010..f864fe49a 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -60,7 +60,7 @@ object Versions { const val shadow = "7.1.3-mirai-modified-SNAPSHOT" const val logback = "1.2.5" - const val slf4j = "1.7.32" + const val slf4j = "2.0.3" const val log4j = "2.17.2" const val asm = "9.4" const val difflib = "1.3.0" diff --git a/mirai-console/backend/mirai-console/build.gradle.kts b/mirai-console/backend/mirai-console/build.gradle.kts index 6bf4ed0e1..338e698ed 100644 --- a/mirai-console/backend/mirai-console/build.gradle.kts +++ b/mirai-console/backend/mirai-console/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { smartImplementation(`maven-resolver-impl`) smartImplementation(`maven-resolver-connector-basic`) smartImplementation(`maven-resolver-transport-http`) + smartImplementation(`slf4j-api`) smartApi(`kotlinx-coroutines-jdk8`) testApi(project(":mirai-core")) diff --git a/mirai-console/backend/mirai-console/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/mirai-console/backend/mirai-console/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100644 index 000000000..47e380b3c --- /dev/null +++ b/mirai-console/backend/mirai-console/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +net.mamoe.mirai.console.internal.logging.externalbind.slf4j.MiraiConsoleSLF4JService diff --git a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt index 45e09d9f7..90f82f69e 100644 --- a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt +++ b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt @@ -42,6 +42,7 @@ import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.extension.GlobalComponentStorageImpl import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl import net.mamoe.mirai.console.internal.logging.MiraiConsoleLogger +import net.mamoe.mirai.console.internal.logging.externalbind.slf4j.MiraiConsoleSLF4JAdapter import net.mamoe.mirai.console.internal.permission.BuiltInPermissionService import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl import net.mamoe.mirai.console.internal.shutdown.ShutdownDaemon @@ -278,6 +279,10 @@ ___ ____ _ _____ _ } } + phase("initialize logging bridges") { + MiraiConsoleSLF4JAdapter.doSlf4JInit() + } + phase("initialize all plugins") { pluginManager // init diff --git a/mirai-console/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt b/mirai-console/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt index 0edaaff5a..991a1c2c9 100644 --- a/mirai-console/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt +++ b/mirai-console/backend/mirai-console/src/internal/data/builtins/LoggerConfig.kt @@ -38,11 +38,14 @@ public class LoggerConfig : ReadOnlyPluginConfig("Logger") { "example.logger" to AbstractLoggerController.LogPriority.NONE, "console.debug" to AbstractLoggerController.LogPriority.NONE, "Bot" to AbstractLoggerController.LogPriority.ALL, + "org.eclipse.aether.internal" to AbstractLoggerController.LogPriority.INFO, + "org.apache.http.wire" to AbstractLoggerController.LogPriority.INFO, ) ) @Serializable public class Binding @MiraiExperimentalApi public constructor( + public val slf4j: Boolean = true, ) @ValueDescription( diff --git a/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/MiraiConsoleSLF4JService.kt b/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/MiraiConsoleSLF4JService.kt new file mode 100644 index 000000000..40e736cf4 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/MiraiConsoleSLF4JService.kt @@ -0,0 +1,98 @@ +/* + * 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.internal.logging.externalbind.slf4j + +import net.mamoe.mirai.console.MiraiConsoleImplementation.ConsoleDataScope.Companion.get +import net.mamoe.mirai.console.internal.data.builtins.DataScope +import net.mamoe.mirai.console.internal.data.builtins.LoggerConfig +import net.mamoe.mirai.utils.MiraiLogger +import net.mamoe.mirai.utils.safeCast +import org.slf4j.ILoggerFactory +import org.slf4j.IMarkerFactory +import org.slf4j.event.SubstituteLoggingEvent +import org.slf4j.helpers.BasicMarkerFactory +import org.slf4j.helpers.NOPLoggerFactory +import org.slf4j.helpers.NOPMDCAdapter +import org.slf4j.helpers.SubstituteLoggerFactory +import org.slf4j.spi.MDCAdapter +import org.slf4j.spi.SLF4JServiceProvider + +@PublishedApi +internal class MiraiConsoleSLF4JService : SLF4JServiceProvider { + private val basicMarkerFactory = BasicMarkerFactory() + private val nopMDCAdapter = NOPMDCAdapter() + private val dfactory = ILoggerFactory { MiraiConsoleSLF4JAdapter.getCurrentLogFactory().getLogger(it) } + + override fun getMarkerFactory(): IMarkerFactory = basicMarkerFactory + override fun getMDCAdapter(): MDCAdapter = nopMDCAdapter + override fun getRequestedApiVersion(): String = "2.0" + override fun getLoggerFactory(): ILoggerFactory = dfactory + override fun initialize() {} +} + +internal object MiraiConsoleSLF4JAdapter { + /** + * Used before mirai-console start + */ + private val substituteServiceFactory = SubstituteLoggerFactory() + + @Volatile + private var initialized: Boolean = false + + @Volatile + private var currentLoggerFactory: ILoggerFactory = substituteServiceFactory + + internal fun getCurrentLogFactory(): ILoggerFactory { + if (initialized) return currentLoggerFactory + + synchronized(MiraiConsoleSLF4JAdapter::class.java) { + return currentLoggerFactory + } + } + + internal fun doSlf4JInit() { + synchronized(MiraiConsoleSLF4JAdapter::class.java) { + val logConfig = DataScope.get() + + currentLoggerFactory = if (logConfig.binding.slf4j) { + ILoggerFactory { ident -> + SLF4JAdapterLogger(MiraiLogger.Factory.create(MiraiConsoleSLF4JAdapter::class.java, ident)) + } + } else { + NOPLoggerFactory() + } + initialized = true + + // region relay events + + substituteServiceFactory.postInitialization() + substituteServiceFactory.loggers.forEach { slog -> + slog.setDelegate(currentLoggerFactory.getLogger(slog.name)) + } + + substituteServiceFactory.eventQueue.let { queue -> + for (event in queue) { + replaySingleEvent(event) + } + } + substituteServiceFactory.clear() + // endregion + } + } + + + private fun replaySingleEvent(event: SubstituteLoggingEvent?) { + if (event == null) return + val substLogger = event.logger + + substLogger.delegate().safeCast()?.process(event) + } + +} diff --git a/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/SLF4JAdapterLogger.kt b/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/SLF4JAdapterLogger.kt new file mode 100644 index 000000000..0bf6ce867 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/logging/externalbind/slf4j/SLF4JAdapterLogger.kt @@ -0,0 +1,304 @@ +/* + * 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.internal.logging.externalbind.slf4j + +import net.mamoe.mirai.utils.MiraiLogger +import org.slf4j.Logger +import org.slf4j.Marker +import org.slf4j.event.SubstituteLoggingEvent +import java.lang.invoke.MethodHandle +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.nio.CharBuffer +import java.text.MessageFormat +import java.util.regex.Pattern +import org.slf4j.event.Level as SLF4JEventLevel + +@Suppress("RegExpRedundantEscape") +internal class SLF4JAdapterLogger( + private val logger: MiraiLogger +) : Logger { + // Copied from Log4J + internal companion object { + private const val FORMAT_SPECIFIER = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])" + + private val MSG_PATTERN = Pattern.compile(FORMAT_SPECIFIER) + private const val DELIM_START = '{' + private const val DELIM_STOP = '}' + private const val ESCAPE_CHAR = '\\' + + @JvmStatic + internal fun String.simpleFormat(args: Array): String { + val buffer = StringBuilder() + val reader = CharBuffer.wrap(this) + var isEscape = false + var index = 0 + while (reader.hasRemaining()) { + when (val next = reader.get()) { + ESCAPE_CHAR -> { + if (isEscape) { + buffer.append(ESCAPE_CHAR) + } + isEscape = !isEscape + } + DELIM_START -> { + if (isEscape) { + buffer.append(next) + } else { + if (reader.hasRemaining()) { + if (reader.get(reader.position()) == DELIM_STOP) { + reader.get() + buffer.append(args.getOrNull(index)) + index++ + } else { + buffer.append(DELIM_START) + } + } else { + buffer.append(DELIM_START) + } + } + isEscape = false + } + else -> buffer.append(next).also { isEscape = false } + } + } + return buffer.toString() + } + + internal fun String.format1(vararg arguments: Any?): String = format2(arguments) + + // (java.lang.String, java.lang.Object[]): java.lang.String + @Suppress("LocalVariableName") + private val formatWithLog4JMH: MethodHandle? = kotlin.runCatching { + val c_ParameterizedMessage = Class.forName("org.apache.logging.log4j.message.ParameterizedMessage") + + val mhLookup = MethodHandles.lookup() + val mh_newParameterizedMessage = mhLookup.findConstructor( + c_ParameterizedMessage, MethodType.methodType(Void.TYPE, String::class.java, Array::class.java) + ).asFixedArity() + + val mh_getFormattedMessage = mhLookup.findVirtual( + c_ParameterizedMessage, "getFormattedMessage", MethodType.methodType(String::class.java) + ) + + MethodHandles.filterReturnValue(mh_newParameterizedMessage, mh_getFormattedMessage) + + }.getOrNull() + + @JvmStatic + internal fun String.format2(args: Array): String { + kotlin.runCatching { + val formatter = MessageFormat(this) + val formats = formatter.formats + if (formats.isNotEmpty()) { + return formatter.format(args) + } + } + kotlin.runCatching { + if (MSG_PATTERN.matcher(this).find()) { + return String.format(this, *args) + } + } + kotlin.runCatching { + // Try format with Log4J + formatWithLog4JMH?.let { formatWithLog4JMH -> + return formatWithLog4JMH.invoke(this@format2, args) as String + } + } + return simpleFormat(args) + } + } + + ////////////////////////////////////////////////////////////////// + override fun isTraceEnabled(): Boolean = logger.isVerboseEnabled + override fun isTraceEnabled(marker: Marker?): Boolean = logger.isVerboseEnabled + override fun isDebugEnabled(): Boolean = logger.isDebugEnabled + override fun isDebugEnabled(marker: Marker?): Boolean = logger.isDebugEnabled + override fun isInfoEnabled(): Boolean = logger.isInfoEnabled + override fun isInfoEnabled(marker: Marker?): Boolean = logger.isInfoEnabled + override fun isWarnEnabled(): Boolean = logger.isWarningEnabled + override fun isWarnEnabled(marker: Marker?): Boolean = logger.isWarningEnabled + override fun isErrorEnabled(): Boolean = logger.isErrorEnabled + override fun isErrorEnabled(marker: Marker?): Boolean = logger.isErrorEnabled + ////////////////////////////////////////////////////////////////// + + override fun getName(): String = logger.identity ?: "" + + @Suppress("DuplicatedCode") + internal fun process(event: SubstituteLoggingEvent) { + val msg = event.message + val argx = event.argumentArray + val throwx = event.throwable + + val evtlv = event.level ?: return + + val isEnabled = when (evtlv) { + SLF4JEventLevel.ERROR -> isErrorEnabled + SLF4JEventLevel.WARN -> isWarnEnabled + SLF4JEventLevel.INFO -> isInfoEnabled + SLF4JEventLevel.DEBUG -> isDebugEnabled + SLF4JEventLevel.TRACE -> isTraceEnabled + } + if (!isEnabled) return + + if (argx == null) { + when (evtlv) { + SLF4JEventLevel.ERROR -> error(msg, t = throwx) + SLF4JEventLevel.WARN -> warn(msg, t = throwx) + SLF4JEventLevel.INFO -> info(msg, t = throwx) + SLF4JEventLevel.DEBUG -> debug(msg, t = throwx) + SLF4JEventLevel.TRACE -> trace(msg, t = throwx) + } + return + } + + if (throwx == null) { + when (evtlv) { + SLF4JEventLevel.ERROR -> error(msg, arguments = argx) + SLF4JEventLevel.WARN -> warn(msg, arguments = argx) + SLF4JEventLevel.INFO -> info(msg, arguments = argx) + SLF4JEventLevel.DEBUG -> debug(msg, arguments = argx) + SLF4JEventLevel.TRACE -> trace(msg, arguments = argx) + } + return + } + + val msg2 = msg.format2(argx) + when (evtlv) { + SLF4JEventLevel.ERROR -> error(msg2, t = throwx) + SLF4JEventLevel.WARN -> warn(msg2, t = throwx) + SLF4JEventLevel.INFO -> info(msg2, t = throwx) + SLF4JEventLevel.DEBUG -> debug(msg2, t = throwx) + SLF4JEventLevel.TRACE -> trace(msg2, t = throwx) + } + } + + ////////////////////////////////////////////////////////////////// + + private inline fun iT(a: () -> Unit) { + if (isTraceEnabled) a() + } + + private inline fun iD(a: () -> Unit) { + if (isDebugEnabled) a() + } + + private inline fun iI(a: () -> Unit) { + if (isInfoEnabled) a() + } + + private inline fun iW(a: () -> Unit) { + if (isWarnEnabled) a() + } + + private inline fun iE(a: () -> Unit) { + if (isErrorEnabled) a() + } + + ////////////////////////////////////////////////////////////////// + + override fun trace(msg: String?) { + logger.verbose(msg) + } + + override fun trace(msg: String?, t: Throwable?) { + logger.verbose(msg, t) + } + + override fun trace(format: String, arg: Any?) = iT { trace(format.format1(arg)) } + override fun trace(format: String, arg1: Any?, arg2: Any?) = iT { trace(format.format1(arg1, arg2)) } + override fun trace(format: String, arguments: Array) = iT { trace(format.format2(arguments)) } + + override fun trace(marker: Marker?, msg: String?) = trace(msg) + override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg) + override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = trace(format, arg1, arg2) + override fun trace(marker: Marker?, format: String, argArray: Array) = trace(format, argArray) + override fun trace(marker: Marker?, msg: String?, t: Throwable?) = trace(msg, t) + + ////////////////////////////////////////////////////////////////// + + override fun debug(msg: String?) { + logger.debug(msg) + } + + override fun debug(msg: String?, t: Throwable?) { + logger.debug(msg, t) + } + + override fun debug(format: String, arg: Any?) = iD { debug(format.format1(arg)) } + override fun debug(format: String, arg1: Any?, arg2: Any?) = iD { debug(format.format1(arg1, arg2)) } + override fun debug(format: String, arguments: Array) = iD { debug(format.format2(arguments)) } + + override fun debug(marker: Marker?, msg: String?) = debug(msg) + override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg) + override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = debug(format, arg1, arg2) + override fun debug(marker: Marker?, format: String, arguments: Array) = debug(format, arguments) + override fun debug(marker: Marker?, msg: String?, t: Throwable?) = debug(msg, t) + + ////////////////////////////////////////////////////////////////// + + override fun info(msg: String?) { + logger.info(msg) + } + + override fun info(msg: String?, t: Throwable?) { + logger.info(msg, t) + } + + override fun info(format: String, arg: Any?) = iI { info(format.format1(arg)) } + override fun info(format: String, arg1: Any?, arg2: Any?) = iI { info(format.format1(arg1, arg2)) } + override fun info(format: String, arguments: Array) = iI { info(format.format2(arguments)) } + + override fun info(marker: Marker?, msg: String?) = info(msg) + override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg) + override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = info(format, arg1, arg2) + override fun info(marker: Marker?, format: String, arguments: Array) = info(format, arguments) + override fun info(marker: Marker?, msg: String?, t: Throwable?) = info(msg, t) + + ////////////////////////////////////////////////////////////////// + + override fun warn(msg: String?) { + logger.warning(msg) + } + + override fun warn(msg: String?, t: Throwable?) { + logger.warning(msg, t) + } + + override fun warn(format: String, arg: Any?) = iW { warn(format.format1(arg)) } + override fun warn(format: String, arguments: Array) = iW { warn(format.format2(arguments)) } + override fun warn(format: String, arg1: Any?, arg2: Any?) = iW { warn(format.format1(arg1, arg2)) } + + override fun warn(marker: Marker?, msg: String?) = warn(msg) + override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg) + override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = warn(format, arg1, arg2) + override fun warn(marker: Marker?, format: String, arguments: Array) = warn(format, arguments) + override fun warn(marker: Marker?, msg: String?, t: Throwable?) = warn(msg, t) + + ////////////////////////////////////////////////////////////////// + + override fun error(msg: String?) { + logger.error(msg) + } + + override fun error(msg: String?, t: Throwable?) { + logger.error(msg, t) + } + + override fun error(format: String, arg: Any?) = iE { error(format.format1(arg)) } + override fun error(format: String, arg1: Any?, arg2: Any?) = iE { error(format.format1(arg1, arg2)) } + override fun error(format: String, arguments: Array) = iE { error(format.format2(arguments)) } + + override fun error(marker: Marker?, msg: String?) = error(msg) + override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg) + override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = error(format, arg1, arg2) + override fun error(marker: Marker?, format: String, arguments: Array) = error(format, arguments) + override fun error(marker: Marker?, msg: String?, t: Throwable?) = error(msg, t) +} diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt index df7baa6cc..a68eb299d 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt @@ -129,7 +129,12 @@ internal class DynLibClassLoader : DynamicClasspathClassLoader { if (name in AllDependenciesClassesHolder.allclasses) { return AllDependenciesClassesHolder.appClassLoader.loadClass(name) } - if (name.startsWith("net.mamoe.mirai.") || name.startsWith("kotlin.") || name.startsWith("kotlinx.")) { // Avoid plugin classing cheating + if ( + name.startsWith("net.mamoe.mirai.") + || name.startsWith("kotlin.") + || name.startsWith("kotlinx.") + || name.startsWith("org.slf4j.") + ) { // Avoid plugin classing cheating try { return AllDependenciesClassesHolder.appClassLoader.loadClass(name) } catch (ignored: ClassNotFoundException) { diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt index c9a080d8d..25732474e 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt @@ -80,6 +80,11 @@ internal class JvmPluginDependencyDownloader( ) return@DependencyFilter false } + // Re-download slf4j-api is unnecessary since slf4j-api was bound by console + if (artGroup == "org.slf4j" && artId == "slf4j-api") { + return@DependencyFilter false + } + // Loaded by console system if ("$artGroup:$artId" in MiraiConsoleBuildDependencies.dependencies) return@DependencyFilter false