Browse Source

ref: Logger v2

龙腾猫跃 5 months ago
parent
commit
cbec2acacd
2 changed files with 227 additions and 3 deletions
  1. 102 0
      Shared/Extensions/ExceptionExtensions.cs
  2. 125 3
      Shared/General/Logger.cs

+ 102 - 0
Shared/Extensions/ExceptionExtensions.cs

@@ -0,0 +1,102 @@
+namespace MeloongCore.Extensions;
+public static class ExceptionExtensions {
+    /// <summary>
+    /// 提取 <paramref name="ex"/> 与其 InnerException 的详细描述与堆栈信息。返回内容总是多于一行。
+    /// </summary>
+    /// <param name="showAllStacks">是否必须显示所有堆栈。通常用于判定堆栈信息。</param>
+    public static string GetDetail(this Exception? ex, bool showAllStacks = false) {
+        if (ex is null) return "无可用错误信息!";
+
+        // 获取最底层的异常
+        var outerEx = ex;
+        var innerEx = ex;
+        while (innerEx.InnerException is not null) innerEx = innerEx.InnerException;
+
+        // 获取各级错误的描述与堆栈信息
+        var descList = new List<string>();
+        bool isInner = false;
+        while (ex is not null) {
+            descList.Add((isInner ? "→ " : "") + ex.Message.ReplaceLineEndings("\r\n", true));
+            if (ex.StackTrace is not null) {
+                foreach (string stack in ex.StackTrace.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) {
+                    if (showAllStacks || stack.ContainsIgnoreCase("pcl"))
+                        descList.Add(stack.ReplaceLineEndings(""));
+                }
+            }
+            if (ex.GetType().FullName != "System.Exception")
+                descList.Add("   错误类型:" + ex.GetType().FullName);
+            ex = ex.InnerException;
+            isInner = true;
+        }
+
+        // 构造输出信息
+        string? usualReason = AnalyzeUsualReason(innerEx, outerEx, descList);
+        if (usualReason is not null) {
+            return usualReason + "\r\n\r\n————————————\r\n详细错误信息:\r\n" + descList.Join("\r\n");
+        } 
+        return descList.Join("\r\n");
+    }
+
+    /// <summary>
+    /// 提取 <paramref name="ex"/> 与其 InnerException 的描述,汇总到一行。
+    /// </summary>
+    public static string GetBrief(this Exception? ex) {
+        if (ex is null) return "无可用错误信息!";
+
+        // 获取最底层的异常
+        var outerEx = ex;
+        var innerEx = ex;
+        while (innerEx.InnerException is not null) innerEx = innerEx.InnerException;
+
+        // 获取各级错误的描述
+        var descList = new List<string>();
+        while (ex is not null) {
+            descList.Add(ex.Message.ReplaceLineEndings(" ", true));
+            ex = ex.InnerException;
+        }
+        descList = descList.Distinct().ToList();
+
+        // 构造输出信息
+        string? usualReason = AnalyzeUsualReason(innerEx, outerEx, descList);
+        if (usualReason is not null)
+            return usualReason + "详细错误:" + descList.First();
+        descList.Reverse();
+        return descList.Join(" ← ");
+    }
+
+    private static string? AnalyzeUsualReason(Exception innerEx, Exception outerEx, List<string> descList) {
+        if (innerEx is TypeLoadException or BadImageFormatException or MissingMethodException or NotImplementedException or TypeInitializationException)
+            return "PCL 的运行环境存在问题。请尝试重新安装 .NET Framework 4.8 然后再试。若无法安装,请先卸载较新版本的 .NET Framework,然后再尝试安装。";
+        if (innerEx is UnauthorizedAccessException)
+            return "PCL 的权限不足。请尝试右键 PCL 选择以管理员身份运行。";
+        if (innerEx is OutOfMemoryException)
+            return "你的电脑运行内存不足,导致 PCL 无法继续运行。请在关闭一部分不需要的程序后再试。";
+        if (innerEx is COMException)
+            return "由于操作系统或显卡存在问题,导致出现错误。请尝试重启 PCL。";
+        if (innerEx is System.Net.Sockets.SocketException && descList.Any(l => l.Contains("WSAStartup")))
+            return "请尝试卸载中国移动云盘,然后再试。";
+        if (outerEx.IsNetworkRelated())
+            return "你的网络环境不佳,请稍后再试,或使用 VPN 改善网络环境。";
+        return null;
+    }
+
+    /// <summary>
+    /// 判断某个 <paramref name="ex"/> 是否为网络问题所导致。
+    /// </summary>
+    public static bool IsNetworkRelated(this Exception ex) {
+        // 提取 Message(不能用 GetDetail,因为堆栈的方法参数中就有 timeout 字样)
+        string detail = "";
+        var cur = ex;
+        detail += cur.Message;
+        while (cur.InnerException is not null) {
+            cur = cur.InnerException;
+            detail += cur.Message;
+        }
+        // 判断
+        if (detail.Contains("(403)")) return false;
+        return new[] {
+            "(408)", "超时", "timeout", "网络请求失败", "连接尝试失败", "远程主机强迫关闭了",
+            "远程方已关闭传输流", "未能解析此远程名称", "由于目标计算机积极拒绝", "基础连接已经关闭"
+        }.Any(k => detail.ContainsIgnoreCase(k));
+    }
+}

+ 125 - 3
Shared/General/Logger.cs

@@ -1,4 +1,7 @@
-namespace MeloongCore;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+
+namespace MeloongCore;
 
 /// <summary>
 /// 日志等级。
@@ -77,9 +80,128 @@ public static class Logger {
     public static void Error(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Error, behavior, filePath);
 }
 
+/// <summary>
+/// 将日志输出到 <see cref="Debug"/> 的 <see cref="ILogger"/> 基础实现。
+/// </summary>
 public class ConsoleLogger : ILogger {
-    public void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
+    /// <summary>
+    /// 在写入日志前对文本进行预处理的处理器。
+    /// </summary>
+    public event Func<string, string>? TextProcessor;
+
+    /// <summary>
+    /// 最低输出等级,低于此等级的日志将被忽略。默认为 <see cref="LogLevels.Trace"/>。
+    /// </summary>
+    public LogLevels MinLevel { get; set; } = LogLevels.Trace;
+
+    public virtual void Log(string message, LogLevels level, LogBehaviors behavior, string filePath) {
+        if (level < MinLevel) return;
+        try {
+            Emit(message, level, filePath);
+        } catch { }
+    }
+    public virtual void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
+        if (level < MinLevel) return;
+        if (ex is ThreadInterruptedException) return;
+        try {
+            Emit((message is null ? "" : $"{message}:") + ex.GetDetail(true), level, filePath);
+        } catch { }
     }
-    public void Log(Exception ex, string? message, LogLevels level, LogBehaviors behavior, string filePath) {
+
+    private void Emit(string body, LogLevels level, string filePath) {
+        // 构造原始文本
+        string threadName = Thread.CurrentThread.Name is { Length: > 0 } n ? n : "主线程";
+        string file = filePath.AfterLast("\\").BeforeFirst(".");
+        if (file.StartsWithF("Mod")) file = file.AfterFirst("Mod");
+        string text = $"<{threadName}> [{file}] {body}\r\n";
+
+        // 应用处理器
+        if (TextProcessor is not null) foreach (var h in TextProcessor.GetInvocationList().OfType<Func<string, string>>()) text = h(text);
+
+        string timestamp = $"{DateTime.Now:HH':'mm':'ss'.'fff} | ";
+        string debugPrefix = level switch { LogLevels.Trace => "T ", LogLevels.Warning => "W ", LogLevels.Error => "E ", _ => "I " };
+        string formatted = text
+            .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
+            .Select(t => { t = debugPrefix + timestamp + t; return t; })
+            .Join("\r\n");
+
+        WriteOutput(formatted, level);
+    }
+
+    /// <summary>
+    /// 将格式化后的日志文本写入额外目标。
+    /// <see cref="ConsoleLogger"/> 不执行额外操作(已由 <see cref="Emit"/> 写入调试控制台);
+    /// 子类可重写此方法以追加写入更多目标。
+    /// </summary>
+    protected virtual void WriteOutput(string formattedText, LogLevels level) {
+        Debug.WriteLine(formattedText);
     }
 }
+
+/// <summary>
+/// 在 <see cref="ConsoleLogger"/> 基础上将日志缓冲写入文件的 <see cref="ILogger"/> 实现。
+/// </summary>
+public class FileLogger : ConsoleLogger {
+    private readonly ConcurrentQueue<string> logQueue = new();
+    private StreamWriter? logWriter;
+
+    /// <inheritdoc/>
+    protected override void WriteOutput(string formattedText, LogLevels level) {
+        base.WriteOutput(formattedText, level);
+        logQueue.Enqueue(formattedText);
+    }
+
+    /// <summary>
+    /// 启动日志文件写入。轮换历史日志文件,随后在后台线程持续将缓冲区内容写入 Log1.txt。
+    /// </summary>
+    /// <param name="logFolder">日志文件夹路径,以 \ 结尾。</param>
+    /// <param name="onInitFailed">初始化失败时的回调,参数为捕获到的异常。</param>
+    public void LogStart(string logFolder, Action<Exception>? onInitFailed = null) {
+        var thread = new Thread(() => {
+            bool isInitSuccess = true;
+            try {
+                for (int i = 4; i >= 1; i--) {
+                    if (FileUtils.Exists(logFolder + $"Log{i}.txt")) {
+                        if (FileUtils.Exists(logFolder + $"Log{i + 1}.txt"))
+                            FileUtils.Delete(logFolder + $"Log{i + 1}.txt");
+                        FileUtils.Copy(logFolder + $"Log{i}.txt", logFolder + $"Log{i + 1}.txt");
+                    }
+                }
+                FileUtils.CreateAsStream(logFolder + "Log1.txt").Dispose();
+            } catch (Exception ex) {
+                isInitSuccess = false;
+                onInitFailed?.Invoke(ex);
+            }
+            try {
+                logWriter = new StreamWriter(logFolder + "Log1.txt", append: true) { AutoFlush = true };
+            } catch {
+                logWriter = null;
+            }
+            while (true) {
+                if (isInitSuccess)
+                    LogFlush();
+                else
+                    while (logQueue.TryDequeue(out _)) { } // 清空队列避免内存爆炸
+                Thread.Sleep(50);
+            }
+        }) {
+            Name = "Log Writer",
+            Priority = ThreadPriority.Lowest,
+            IsBackground = true,
+        };
+        thread.Start();
+    }
+
+    /// <summary>
+    /// 将日志队列中的内容立即写入文件。
+    /// </summary>
+    public void LogFlush() {
+        try {
+            if (logWriter is null || logQueue.IsEmpty) return;
+            var sb = new StringBuilder();
+            while (logQueue.TryDequeue(out string? line))
+                sb.Append(line).Append("\r\n");
+            logWriter.Write(sb);
+        } catch { }
+    }
+}