diff --git a/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt b/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt index 67b45b9b7..be92004bc 100644 --- a/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt +++ b/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt @@ -72,7 +72,7 @@ internal object MiraiConsoleImplementationBridge : CoroutineScope, MiraiConsoleI MiraiConsole { override val pluginCenter: PluginCenter get() = throw UnsupportedOperationException("PluginCenter is not supported yet") - private val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance + internal val instance: MiraiConsoleImplementation by MiraiConsoleImplementation.Companion::instance override val buildDate: Instant by MiraiConsoleBuildConstants::buildDate override val version: SemVersion by MiraiConsoleBuildConstants::version override val rootPath: Path by instance::rootPath diff --git a/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt b/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt index a9e9a1f2c..a099e3515 100644 --- a/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt +++ b/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.console.internal.data import kotlinx.serialization.json.Json import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.data.* +import net.mamoe.mirai.console.internal.util.report.ReportGenerator import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.SilentLogger @@ -71,17 +72,40 @@ internal open class MultiFilePluginDataStorageImpl( @ConsoleExperimentalApi public override fun store(holder: PluginDataHolder, instance: PluginData) { + var yamlRendered: String? = null getPluginDataFile(holder, instance).writeText( kotlin.runCatching { yaml.encodeToString(instance.updaterSerializer, Unit).also { + yamlRendered = it yaml.decodeAnyFromString(it) // test yaml + error("Test error") } - }.recoverCatching { + }.recoverCatching { exception -> // Just use mainLogger for convenience. MiraiConsole.mainLogger.warning( "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " + "Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new", - it + exception + ) + val reportPath = ReportGenerator.generateReport("YamlKt-Format-") { + pw.println("Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. ") + pw.println("Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new") + pw.println() + yamlRendered?.let { + title("Rendered YAML") + pw.println(it) + pw.println() + } + title("Exception") + renderException(exception) + renderCurrentThread() + } + MiraiConsole.mainLogger.warning( + "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " + + "Please report this exception and relevant configurations to https://github.com/mamoe/mirai-console/issues/new" + ) + MiraiConsole.mainLogger.warning( + "Error Report location: $reportPath" ) json.encodeToString(instance.updaterSerializer, Unit) }.getOrElse { diff --git a/backend/mirai-console/src/internal/util/report/ReportGenerator.kt b/backend/mirai-console/src/internal/util/report/ReportGenerator.kt new file mode 100644 index 000000000..35f3b9442 --- /dev/null +++ b/backend/mirai-console/src/internal/util/report/ReportGenerator.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.internal.util.report + +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants +import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.internal.data.isDirectory +import net.mamoe.mirai.console.internal.data.isFile +import net.mamoe.mirai.console.internal.data.mkdir +import net.mamoe.mirai.console.internal.plugin.PluginManagerImpl +import net.mamoe.mirai.console.permission.PermissionService +import net.mamoe.mirai.console.plugin.PluginManager.INSTANCE.description +import java.io.* +import java.lang.management.LockInfo +import java.lang.management.ManagementFactory +import java.lang.management.MonitorInfo +import java.lang.management.ThreadInfo +import java.nio.file.Files +import java.nio.file.Path +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Suppress("unused") +internal class ReportGenerator( + val pw: PrintWriter +) : Closeable { + companion object { + internal val threadMXBean = ManagementFactory.getThreadMXBean() + internal val directory by lazy { + MiraiConsole.rootPath.resolve("error-reports") + } + + fun ThreadInfo.dumpTo(sb: Appendable) { + sb.run { + append('\"') + append(threadName) + append("\" Id=") + append(threadId.toString()) + append(" ") + append(threadState.toString()) + + lockName?.let { append(" on ").append(it) } + lockOwnerName?.let { append(" owned by \"").append(it).append("\" Id=").append(lockOwnerId.toString()) } + if (isSuspended) { + sb.append(" (suspended)") + } + if (isInNative) { + sb.append(" (in native)") + } + sb.append('\n') + var i = 0 + while (i < stackTrace.size) { + val ste: StackTraceElement = stackTrace[i] + sb.append("\tat $ste") + sb.append('\n') + if (i == 0 && lockInfo != null) { + when (threadState) { + Thread.State.BLOCKED -> { + sb.append("\t- blocked on $lockInfo") + sb.append('\n') + } + Thread.State.WAITING -> { + sb.append("\t- waiting on $lockInfo") + sb.append('\n') + } + Thread.State.TIMED_WAITING -> { + sb.append("\t- waiting on $lockInfo") + sb.append('\n') + } + else -> { + } + } + } + for (mi: MonitorInfo in lockedMonitors) { + if (mi.lockedStackDepth == i) { + sb.append("\t- locked $mi") + sb.append('\n') + } + } + i++ + } + val locks: Array = lockedSynchronizers + if (locks.isNotEmpty()) { + sb.append("\n\tNumber of locked synchronizers = " + locks.size) + sb.append('\n') + for (li: LockInfo in locks) { + sb.append("\t- $li") + sb.append('\n') + } + } + } + } + + fun generateToString(action: ReportGenerator.() -> Unit): String { + return StringWriter().apply { + ReportGenerator(PrintWriter(this)).use(action) + }.toString() + } + + fun generateReport( + prefix: String = "", + action: ReportGenerator.() -> Unit + ): Path { + val now = System.currentTimeMillis() + var counter = 0 + var outputName = "$prefix$now.log" + directory.mkdir() + var path: Path + do { + path = directory.resolve(outputName) + if (!path.isFile && !path.isDirectory) { + break + } + outputName = "$prefix$now-$counter.log" + counter++ + } while (true) + ReportGenerator(PrintWriter(BufferedWriter(OutputStreamWriter(Files.newOutputStream(path))))) + .use(action) + return path + } + } + + fun renderCurrentThread() { + title("Current Thread") + renderThread(Thread.currentThread()) + } + + fun renderThread(thread: Thread) { + threadMXBean.getThreadInfo( + longArrayOf(thread.id), + true, + true + )[0].dumpTo(pw) + } + + fun title(title: String) { + pw.append("=============== [ ").append(title).append(" ] ===============") + pw.println() + } + + fun dumpSystemEnv() { + title("System Env") + + pw.println("SysEnv") + pw.println() + pw.println("```") + System.getenv().forEach { (key, value) -> + pw.println("$key\t=\t$value") + } + pw.println("```") + pw.println() + pw.println("JavaProp") + pw.println() + pw.println("```") + System.getProperties().store(pw, null) + pw.println("```") + pw.println() + } + + fun dumpConsoleEnv() { + title("Mirai Console Env") + val buildDateFormatted = + MiraiConsoleBuildConstants.buildDate.atZone(ZoneId.systemDefault()) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + pw.append("MiraiConsole v${MiraiConsoleBuildConstants.versionConst}, built on ") + .append(buildDateFormatted) + .append(".\n") + pw.println("FrontEnd:") + pw.append("\t").println(MiraiConsoleImplementationBridge.instance.javaClass.name) + pw.append("\t").println(MiraiConsoleImplementationBridge.frontEndDescription.render()) + pw.println() + pw.println("Plugins:") + PluginManagerImpl.resolvedPlugins.forEach { plugin -> + val desc = plugin.description + pw.append("\t").append(desc.name).append(" v").append(desc.version.toString()).append(" by ").append(desc.author).println() + pw.append("\t\t `-- ").println(plugin.javaClass.name) + } + pw.println() + pw.println("PermissionService: ") + pw.append("\t").println(PermissionService.INSTANCE) + pw.append("\t\t`- ").println(PermissionService.INSTANCE.javaClass) + } + + fun renderException(throwable: Throwable) { + throwable.printStackTrace(pw) + pw.println() + } + + fun hr() { + pw.println("====================================================") + } + + override fun close() { + pw.close() + } +} \ No newline at end of file