2
0

ExceptionExtensions.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. using System.Net.Sockets;
  2. namespace MeloongCore.Extensions;
  3. public static class ExceptionExtensions {
  4. /// <summary>
  5. /// 返回该异常最底层的 <see cref="Exception.InnerException"/>。
  6. /// </summary>
  7. public static Exception RootException(this Exception ex) {
  8. while (ex.InnerException is { } inner) ex = inner;
  9. return ex;
  10. }
  11. /// <summary>
  12. /// 该异常或其子异常是否为 <see cref="OperationCanceledException"/>、<see cref="TaskCanceledException"/> 或 <see cref="ThreadInterruptedException"/>。
  13. /// </summary>
  14. public static bool IsCanceled(this Exception ex) {
  15. foreach (var inner in ex.Flatten()) {
  16. if (inner is OperationCanceledException or TaskCanceledException or ThreadInterruptedException) return true;
  17. }
  18. return false;
  19. }
  20. /// <summary>
  21. /// 当该异常或其子异常是 <see cref="OperationCanceledException"/>、<see cref="TaskCanceledException"/> 或 <see cref="ThreadInterruptedException"/> 时,抛出对应异常。
  22. /// </summary>
  23. public static void ThrowIfCanceled(this Exception ex) {
  24. foreach (var inner in ex.Flatten()) {
  25. if (inner is OperationCanceledException or TaskCanceledException or ThreadInterruptedException) throw inner;
  26. }
  27. }
  28. /// <summary>
  29. /// 将异常所有的 <see cref="Exception.InnerException"/> 展开。
  30. /// 若为 <see cref="AggregateException"/>,也会展开其中所有的子异常。
  31. /// </summary>
  32. public static IEnumerable<Exception> Flatten(this Exception ex) {
  33. var toFlatten = new Stack<Exception>();
  34. toFlatten.Push(ex);
  35. while (toFlatten.Any()) {
  36. var current = toFlatten.Pop();
  37. yield return current;
  38. if (current is AggregateException ag) {
  39. foreach (var inner in ag.InnerExceptions) toFlatten.Push(inner);
  40. } else if (current.InnerException is not null) {
  41. toFlatten.Push(current.InnerException);
  42. }
  43. }
  44. }
  45. /// <summary>
  46. /// 获取 <paramref name="ex"/> 的用户友好描述。
  47. /// 若 <paramref name="multiline"/> 为 true,则返回多行的详细描述与堆栈信息;否则不整理堆栈,仅将 <see cref="Exception.Message"/> 汇总到一行。
  48. /// </summary>
  49. public static string GetDisplay(this Exception? ex, bool multiline) {
  50. if (ex is null) return "无可用错误信息!";
  51. // 提取堆栈信息
  52. var lines = new List<string>();
  53. bool isInnerException = false;
  54. static string getExceptionMessage(Exception e) => e.Message + (e switch {
  55. WebException webEx => $" (Status={webEx.Status})",
  56. SocketException socketEx => $" (SocketErrorCode={socketEx.SocketErrorCode})",
  57. _ => ""
  58. });
  59. foreach (var currentEx in ex.Flatten()) {
  60. if (multiline) {
  61. lines.Add((isInnerException ? "→ " : "") + getExceptionMessage(currentEx).ReplaceLineEndings("\r\n", true));
  62. var stackLines = (currentEx.StackTrace?.SplitLines(true) ?? [])
  63. .Select(l => l.BeforeLast("(") + l.AfterLast(")"))
  64. .Distinct();
  65. lines.AddRange(stackLines);
  66. if (currentEx.GetType() != typeof(Exception)) lines.Add(" 错误类型:" + currentEx.GetType().FullName);
  67. } else {
  68. lines.Add(getExceptionMessage(currentEx).ReplaceLineEndings(" ", true));
  69. }
  70. isInnerException = true;
  71. }
  72. // 分析常见错误原因
  73. string? commonReason = null;
  74. var rootException = ex.RootException();
  75. if (rootException is TypeLoadException or BadImageFormatException or MissingMethodException or NotImplementedException or TypeInitializationException)
  76. commonReason = "运行环境存在问题。请尝试重新安装 .NET Framework 4.8 然后再试。若无法安装,请先卸载较新版本的 .NET Framework,然后再尝试安装。";
  77. else if (rootException is UnauthorizedAccessException)
  78. commonReason = "程序权限不足。请尝试右键程序,选择以管理员身份运行,或将文件移动到其他文件夹。";
  79. else if (rootException is OutOfMemoryException)
  80. commonReason = "系统的运行内存不足。请在关闭一部分不需要的程序后再试。";
  81. else if (rootException is System.Net.Sockets.SocketException && lines.Any(l => l.Contains("WSAStartup")))
  82. commonReason = "请尝试卸载中国移动云盘,然后再试。";
  83. else if (ex.IsBadNetwork())
  84. commonReason = "你的网络环境不佳,请稍后再试,或使用 VPN 改善网络环境。";
  85. // 输出
  86. if (multiline) {
  87. if (commonReason is null) {
  88. return lines.Join("\r\n");
  89. } else {
  90. return commonReason + "\r\n\r\n——————— 详细错误信息 ———————\r\n" + lines.Join("\r\n");
  91. }
  92. } else {
  93. lines = lines.Distinct().ToList();
  94. if (commonReason is null) {
  95. lines.Reverse();
  96. return lines.Join(" ← ");
  97. } else {
  98. return $"{commonReason}({lines.First()})";
  99. }
  100. }
  101. }
  102. /// <summary>
  103. /// 判断该异常是否是由于网络连接不良导致。
  104. /// </summary>
  105. public static bool IsBadNetwork(this Exception ex) {
  106. foreach (var currentEx in ex.Flatten()) {
  107. if (currentEx is TimeoutException) {
  108. return true;
  109. } else if (currentEx is TaskCanceledException) {
  110. return true;
  111. } else if (currentEx is HttpRequestCodeException reqEx) {
  112. if (reqEx.StatusCode is HttpStatusCode.Forbidden) return false;
  113. if (reqEx.StatusCode is HttpStatusCode.RequestTimeout) return true;
  114. } else if (currentEx is WebException webEx) {
  115. if (webEx.Response is HttpWebResponse response) {
  116. if (response.StatusCode is HttpStatusCode.Forbidden) return false;
  117. if (response.StatusCode is HttpStatusCode.RequestTimeout) return true;
  118. }
  119. if (webEx.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure or WebExceptionStatus.ReceiveFailure or
  120. WebExceptionStatus.SendFailure or WebExceptionStatus.ConnectionClosed or WebExceptionStatus.KeepAliveFailure or WebExceptionStatus.Timeout or
  121. WebExceptionStatus.ProxyNameResolutionFailure or WebExceptionStatus.SecureChannelFailure) return true;
  122. } else if (currentEx is SocketException socketEx) {
  123. if (socketEx.SocketErrorCode is SocketError.NetworkDown or SocketError.NetworkUnreachable or SocketError.NetworkReset or SocketError.ConnectionAborted or
  124. SocketError.ConnectionReset or SocketError.TimedOut or SocketError.ConnectionRefused or SocketError.HostUnreachable or
  125. SocketError.HostNotFound or SocketError.TryAgain or SocketError.NoData) return true;
  126. }
  127. }
  128. return false;
  129. }
  130. }