龙腾猫跃 5 месяцев назад
Родитель
Сommit
c017654b6a

+ 21 - 1
Shared/Extensions/ExceptionExtensions.cs

@@ -11,6 +11,24 @@ public static class ExceptionExtensions {
         return ex;
     }
 
+    /// <summary>
+    /// 将异常所有的 <see cref="Exception.InnerException"/> 展开。
+    /// 若为 <see cref="AggregateException"/>,也会展开其中所有的子异常。
+    /// </summary>
+    public static IEnumerable<Exception> Flatten(this Exception ex) {
+        var toFlatten = new Stack<Exception>();
+        toFlatten.Push(ex);
+        while (toFlatten.Any()) {
+            var current = toFlatten.Pop();
+            yield return current;
+            if (current is AggregateException ag) {
+                foreach (var inner in ag.InnerExceptions) toFlatten.Push(inner);
+            } else if (current.InnerException is not null) {
+                toFlatten.Push(current.InnerException);
+            }
+        }
+    }
+
     /// <summary>
     /// 获取 <paramref name="ex"/> 的用户友好描述。
     /// 若 <paramref name="showMultilineStacks"/> 为 true,则返回多行的详细描述与堆栈信息;否则不整理堆栈,仅将 <see cref="Exception.Message"/> 汇总到一行。
@@ -62,7 +80,7 @@ public static class ExceptionExtensions {
                 lines.Reverse();
                 return lines.Join(" ← ");
             } else {
-                return commonReason + "详细错误:" + lines.First();
+                return $"{commonReason}({lines.First()})";
             }
         }
     }
@@ -74,6 +92,8 @@ public static class ExceptionExtensions {
         for (Exception? currentEx = ex; currentEx is not null; currentEx = currentEx.InnerException) {
             if (currentEx is TimeoutException) {
                 return true;
+            } else if (currentEx is TaskCanceledException) {
+                return true;
             } else if (currentEx is HttpRequestCodeException reqEx) {
                 if (reqEx.StatusCode is HttpStatusCode.Forbidden) return false;
                 if (reqEx.StatusCode is HttpStatusCode.RequestTimeout) return true;

+ 26 - 31
Shared/Extensions/StringExtensions.cs

@@ -96,10 +96,11 @@ public static class StringExtensions {
     /// 若原始字符串为空,则返回 {""}。
     /// </summary>
     public static string[] Split(this string fullStr, string splitStr) {
-        if (splitStr.IsSingle())
+        if (splitStr.IsSingle()) {
             return fullStr.Split(splitStr[0]);
-        else
+        } else {
             return fullStr.Split([splitStr], StringSplitOptions.None);
+        }
     }
 
     /// <summary>
@@ -108,10 +109,8 @@ public static class StringExtensions {
     /// </summary>
     public static string BeforeFirst(this string str, [AllowNull] string text, bool ignoreCase = false) {
         int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text!, ignoreCase);
-        if (pos >= 0)
-            return str.Substring(0, pos);
-        else
-            return str;
+        if (pos < 0) return str;
+        return str.Substring(0, pos);
     }
     /// <summary>
     /// 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。
@@ -119,10 +118,8 @@ public static class StringExtensions {
     /// </summary>
     public static string BeforeLast(this string str, [AllowNull] string text, bool ignoreCase = false) {
         int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text!, ignoreCase);
-        if (pos >= 0)
-            return str.Substring(0, pos);
-        else
-            return str;
+        if (pos < 0) return str;
+        return str.Substring(0, pos);
     }
 
     /// <summary>
@@ -131,10 +128,8 @@ public static class StringExtensions {
     /// </summary>
     public static string AfterFirst(this string str, [AllowNull] string text, bool ignoreCase = false) {
         int pos = string.IsNullOrEmpty(text) ? -1 : str.IndexOfF(text!, ignoreCase);
-        if (pos >= 0)
-            return str.Substring(pos + text!.Length);
-        else
-            return str;
+        if (pos < 0) return str;
+        return str.Substring(pos + text!.Length);
     }
     /// <summary>
     /// 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。
@@ -142,10 +137,8 @@ public static class StringExtensions {
     /// </summary>
     public static string AfterLast(this string str, [AllowNull] string text, bool ignoreCase = false) {
         int pos = string.IsNullOrEmpty(text) ? -1 : str.LastIndexOfF(text!, ignoreCase);
-        if (pos >= 0)
-            return str.Substring(pos + text!.Length);
-        else
-            return str;
+        if (pos < 0) return str;
+        return str.Substring(pos + text!.Length);
     }
 
     /// <summary>
@@ -155,18 +148,20 @@ public static class StringExtensions {
     /// </summary>
     public static string Between(this string str, string after, string before, bool ignoreCase = false) {
         int startPos = string.IsNullOrEmpty(after) ? -1 : str.LastIndexOfF(after, ignoreCase);
-        if (startPos >= 0)
+        if (startPos >= 0) {
             startPos += after.Length;
-        else
+        } else {
             startPos = 0;
+        }
 
         int endPos = string.IsNullOrEmpty(before) ? -1 : str.IndexOfF(before, startPos, ignoreCase);
-        if (endPos >= 0)
+        if (endPos >= 0) {
             return str.Substring(startPos, endPos - startPos);
-        else if (startPos > 0)
+        } else if (startPos > 0) {
             return str.Substring(startPos);
-        else
+        } else {
             return str;
+        }
     }
 
     #endregion
@@ -175,20 +170,20 @@ public static class StringExtensions {
 
     /// <summary>
     /// 替换字符串中的内容。
-    /// 仅当需要替换时,才调用 <paramref name="newValue"/> 获取结果字符串。
+    /// 仅当需要替换时,才调用 <paramref name="newValueGetter"/> 获取结果字符串。
     /// </summary>
-    public static string ReplaceOnDemand(this string str, string oldValue, Func<string> newValue) =>
-        str.Contains(oldValue) ? str.Replace(oldValue, newValue()) : str;
+    public static string Replace(this string str, string oldValue, Func<string> newValueGetter) =>
+        str.Contains(oldValue) ? str.Replace(oldValue, newValueGetter()) : str;
 
     /// <summary>
     /// 将字符串中的换行符统一替换为指定字符。
     /// 若指定了 <paramref name="mergeMultiple"/>,会将多个连续换行符仅替换为一个目标内容。
     /// </summary>
-    public static string ReplaceLineEndings(this string input, string newValue, bool mergeMultiple = false) =>
-        Regex.Replace(input,
-            mergeMultiple ? @"(?:\r\n|[\n\r\f\u0085\u2028\u2029])+" : @"\r\n|[\n\r\f\u0085\u2028\u2029]",
-            newValue.Replace("$", "$$"), // 避免识别成捕获组
-            RegexOptions.Compiled);
+    public static string ReplaceLineEndings(this string input, string newValue, bool mergeMultiple = false) {
+        return (mergeMultiple ? regexLineEndingAndMerge : regexLineEnding).Replace(input, newValue.Replace("$", "$$")); // 避免识别成捕获组
+    }
+    private static readonly Regex regexLineEndingAndMerge = new(@"(?:\r\n|[\n\r\f\u0085\u2028\u2029])+", RegexOptions.Compiled);
+    private static readonly Regex regexLineEnding = new(@"\r\n|[\n\r\f\u0085\u2028\u2029]", RegexOptions.Compiled);
 
     #endregion
 

+ 19 - 11
Shared/General/Logger.cs

@@ -9,7 +9,6 @@ namespace MeloongCore;
 public enum LogLevels {
     /// <summary>
     /// 追踪。
-    /// 只在调试状态下输出日志信息。
     /// </summary>
     Trace,
     /// <summary>
@@ -59,19 +58,23 @@ public enum LogBehaviors {
 public static class Logger {
     public static BaseLogger Instance { get; set; } = new();
 
-    // 转发给实例的方法调用
-
-    public static void Log(string message, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "")
+    // 转发给实例的方法包装
+    public static void Log(string message, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") 
         => Instance.Log(message, level, behavior, filePath);
-    public static void Log(Exception ex, string? message = null, LogLevels level = LogLevels.Warning, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "")
-        => Instance.Log(ex, message, level, behavior, filePath);
-
-    public static void Trace(Func<string> message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") { if (Instance.MinLevel <= LogLevels.Trace) Log(message(), LogLevels.Trace, behavior, filePath); }
     public static void Trace(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Trace, behavior, filePath);
     public static void Info(string message, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Info, behavior, filePath);
     public static void Warning(string message, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Warning, behavior, filePath);
     public static void Error(string message, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(message, LogLevels.Error, behavior, filePath);
-    
+
+    public static void Log(Func<string> messageGetter, LogLevels level = LogLevels.Info, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") {
+        if (Instance.MinLevel <= level) Instance.Log(messageGetter(), level, behavior, filePath); }
+    public static void Trace(Func<string> messageGetter, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(messageGetter, LogLevels.Trace, behavior, filePath);
+    public static void Info(Func<string> messageGetter, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(messageGetter, LogLevels.Info, behavior, filePath);
+    public static void Warning(Func<string> messageGetter, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(messageGetter, LogLevels.Warning, behavior, filePath);
+    public static void Error(Func<string> messageGetter, LogBehaviors behavior = LogBehaviors.AlertThenFeedback, [CallerFilePath] string filePath = "") => Log(messageGetter, LogLevels.Error, behavior, filePath);
+
+    public static void Log(Exception ex, string? message = null, LogLevels level = LogLevels.Warning, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") 
+        => Instance.Log(ex, message, level, behavior, filePath);
     public static void Trace(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Trace, behavior, filePath);
     public static void Info(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.None, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Info, behavior, filePath);
     public static void Warning(Exception ex, string? message = null, LogBehaviors behavior = LogBehaviors.ToastIfDebug, [CallerFilePath] string filePath = "") => Log(ex, message, LogLevels.Warning, behavior, filePath);
@@ -113,7 +116,7 @@ public class BaseLogger {
     /// 格式化日志文本。
     /// </summary>
     protected virtual string Format(string text, LogLevels level, string filePath, Exception? ex) {
-        string prefix = $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{filePath.AfterLast(@"\").BeforeFirst(".")}] ";
+        string prefix = $"{DateTime.Now:HH':'mm':'ss'.'fff} {level.ToString().First()} {(Thread.CurrentThread.Name is null ? "" : $"<{Thread.CurrentThread.Name}> ")}[{Path.GetFileName(filePath).BeforeFirst(".")}] ";
         return text
             .ReplaceLineEndings("\n", mergeMultiple: true).Split(['\n'], StringSplitOptions.RemoveEmptyEntries)
             .Select(t => prefix + t)
@@ -139,7 +142,7 @@ public class BaseLogger {
 /// 将日志输出到 <see cref="Debug"/> 并写入指定的文件夹,最多保留 5 个文件。
 /// 不实现 <see cref="LogBehaviors"/>。
 /// </summary>
-public class FileLogger : BaseLogger {
+public class FileLogger : BaseLogger, IDisposable {
 
     /// <inheritdoc/>
     protected override void Output(string formattedText, LogLevels level) {
@@ -205,4 +208,9 @@ public class FileLogger : BaseLogger {
         thread.Start();
     }
 
+    public void Dispose() {
+        writerAvaliable = false;
+        if (writer is not null) ((IDisposable) writer).Dispose();
+        queuedEvent.Dispose();
+    }
 }