龙腾猫跃 5 mesiacov pred
rodič
commit
256db1e973

+ 1 - 1
Shared/Extensions/ExceptionExtensions.cs

@@ -107,7 +107,7 @@ public static class ExceptionExtensions {
                     WebExceptionStatus.ProxyNameResolutionFailure) return true;
             } else if (currentEx is SocketException socketEx) {
                 if (socketEx.SocketErrorCode is SocketError.NetworkDown or SocketError.NetworkUnreachable or SocketError.NetworkReset or SocketError.ConnectionAborted or 
-                    SocketError.ConnectionReset or SocketError.TimedOut or SocketError.ConnectionRefused or SocketError.HostDown or SocketError.HostUnreachable or 
+                    SocketError.ConnectionReset or SocketError.TimedOut or SocketError.ConnectionRefused or SocketError.HostUnreachable or 
                     SocketError.HostNotFound or SocketError.TryAgain or SocketError.NoData) return true;
             }
         }

+ 1 - 1
Shared/Utils/DirectoryUtils.cs

@@ -107,7 +107,7 @@ public static class DirectoryUtils {
                 try {
                     foreach (string filePath in DirectoryUtils.GetFiles(folder, true)) FileUtils.Delete(filePath); // 删除文件
                     foreach (string str in DirectoryUtils.GetDirectories(folder, true)) DeleteInternal(str); // 递归删除子文件夹
-                    Directory.Delete(folder, true); // 删除空文件夹
+                    ResilientUtils.RetryOn<IOException>(() => Directory.Delete(folder, true)); // 最终删除文件夹
                 } catch (DirectoryNotFoundException ex) { // #4549,也可能已被其他线程删除
                     if (DirectoryUtils.Exists(folder)) {
                         Logger.Warn(ex, $"该文件夹可能为孤立的符号链接,尝试直接删除({folder})");

+ 32 - 24
Shared/Utils/FileUtils.cs

@@ -51,29 +51,18 @@ public static class FileUtils {
 
     #region 创建 / 写入
 
-    /// <summary>
-    /// 创建文件,并打开 <see cref="FileStream"/>。
-    /// </summary>
-    public static FileStream CreateAsStream(string filePath) {
-        DirectoryUtils.Create(filePath, isFilePath: true);
-        Logger.Trace($"创建文件流:{filePath}");
-        return new(PathUtils.WithLongPath(filePath), FileMode.Create);
-    }
-
     /// <summary>
     /// 创建文件,并将 <paramref name="text" /> 写入文件。
     /// 若文件已存在,则会覆盖原文件。
     /// </summary>
     public static void Write(string filePath, string text, Encoding? encoding = null) 
         => FileUtils.Write(filePath, (encoding ?? new UTF8Encoding()).GetBytes(text));
-
     /// <summary>
     /// 创建文件,并将 <paramref name="content" /> 写入文件。
     /// 若文件已存在,则会覆盖原文件。
     /// </summary>
     public static void Write(string filePath, IEnumerable<byte> content) 
         => FileUtils.Write(filePath, [.. content]);
-
     /// <summary>
     /// 创建文件,并将 <paramref name="content" /> 写入文件。
     /// 若文件已存在,则会覆盖原文件。
@@ -81,7 +70,8 @@ public static class FileUtils {
     public static void Write(string filePath, byte[] content) {
         DirectoryUtils.Create(filePath, isFilePath: true);
         Logger.Trace($"写入文件:{filePath}({content.Length} 字节)");
-        File.WriteAllBytes(PathUtils.WithLongPath(filePath), content);
+        ResilientUtils.RetryOn<IOException>(() 
+            => File.WriteAllBytes(PathUtils.WithLongPath(filePath), content));
     }
 
     /// <summary>
@@ -95,6 +85,16 @@ public static class FileUtils {
         Logger.Trace($"写入文件:{filePath}({stream.GetType().Name}{(stream.CanSeek ? $" {stream.Length} 字节" : "")})");
         stream.CopyTo(fileStream);
     }
+    /// <summary>
+    /// 创建文件,并打开 <see cref="FileStream"/>。
+    /// 若文件已存在,则会覆盖原文件。
+    /// </summary>
+    public static FileStream CreateAsStream(string filePath) {
+        DirectoryUtils.Create(filePath, isFilePath: true);
+        Logger.Trace($"创建文件流:{filePath}");
+        return ResilientUtils.RetryOn<IOException, FileStream>(()
+            => new FileStream(PathUtils.WithLongPath(filePath), FileMode.Create));
+    }
 
     #endregion
 
@@ -113,7 +113,8 @@ public static class FileUtils {
         }
         DirectoryUtils.Create(destFilePath, isFilePath: true);
         Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
-        File.Copy(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath), true);
+        ResilientUtils.RetryOn<IOException>(()
+            => File.Copy(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath), true));
     }
 
     /// <summary>
@@ -125,7 +126,8 @@ public static class FileUtils {
         DirectoryUtils.Create(destFilePath, isFilePath: true);
         FileUtils.Delete(destFilePath);
         Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
-        File.Move(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath));
+        ResilientUtils.RetryOn<IOException>(()
+            => File.Move(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath)));
     }
 
     #endregion
@@ -141,7 +143,8 @@ public static class FileUtils {
         if (toRecycleBin) {
             DeleteToRecycleBin(filePath);
         } else {
-            File.Delete(PathUtils.WithLongPath(filePath));
+            ResilientUtils.RetryOn<IOException>(()
+                => File.Delete(PathUtils.WithLongPath(filePath)));
         }
     }
 
@@ -150,12 +153,12 @@ public static class FileUtils {
     /// </summary>
     internal static void DeleteToRecycleBin(string target) {
         // 实际的删除方法
-        static void Run(string filePath) {
+        void Run() {
             IShellItem? item = null;
             IFileOperation? op = null;
             try {
                 var iid = typeof(IShellItem).GUID;
-                Marshal.ThrowExceptionForHR(SHCreateItemFromParsingName(PathUtils.WithoutLongPath(filePath), IntPtr.Zero, ref iid, out item));
+                Marshal.ThrowExceptionForHR(SHCreateItemFromParsingName(PathUtils.WithoutLongPath(target), IntPtr.Zero, ref iid, out item));
                 op = (IFileOperation) new FileOperation();
                 op.SetOperationFlags(0x0040 | 0x0010 | 0x0004); // FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT
                 op.DeleteItem(item, IntPtr.Zero);
@@ -169,12 +172,16 @@ public static class FileUtils {
         }
         // 在 STA 线程中执行删除方法
         if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) {
-            Run(target);
+            ResilientUtils.RetryOn<IOException>(Run);
         } else {
             System.Runtime.ExceptionServices.ExceptionDispatchInfo? internalEx = null; // 捕获内部异常
-            var thread = new Thread(
-                () => { try { Run(target); } catch (Exception ex) { internalEx = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex); } }) 
-                { IsBackground = true, Name = nameof(DeleteToRecycleBin) };
+            var thread = new Thread(() => { 
+                try { 
+                    ResilientUtils.RetryOn<IOException>(Run); 
+                } catch (Exception ex) { 
+                    internalEx = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex); 
+                } 
+            }) { IsBackground = true, Name = nameof(DeleteToRecycleBin) };
             thread.SetApartmentState(ApartmentState.STA);
             thread.Start();
             thread.Join();
@@ -258,7 +265,6 @@ public static class FileUtils {
             using var fileStream = FileUtils.ReadAsStream(compressionFile);
             using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
             FileUtils.Write(outFilePath, gzipStream);
-            progressHandler?.Invoke(1);
             return;
         }
         // 解压 zip
@@ -291,7 +297,8 @@ public static class FileUtils {
         DirectoryUtils.Create(outputFullPath, isFilePath: true);
         FileUtils.Delete(outputFullPath);
         Logger.Trace($"将文件夹中的内容压缩为 zip 文件:{sourceDirectory} → {outputFullPath}");
-        ZipFile.CreateFromDirectory(sourceDirectory, outputFullPath);
+        ResilientUtils.RetryOn<IOException>(()
+            => ZipFile.CreateFromDirectory(sourceDirectory, outputFullPath));
     }
 
     /// <summary>
@@ -317,7 +324,8 @@ public static class FileUtils {
         outputFullPath = PathUtils.WithLongPath(outputFullPath);
         DirectoryUtils.Create(outputFullPath, isFilePath: true);
         FileUtils.Delete(outputFullPath);
-        using var archive = ZipFile.Open(outputFullPath, ZipArchiveMode.Create);
+        using var archive = ResilientUtils.RetryOn<IOException, ZipArchive>(()
+            => ZipFile.Open(outputFullPath, ZipArchiveMode.Create));
         Logger.Trace($"创建 zip 文件:{sources.Count} 个文件 → {outputFullPath}\n{sources.Select(p => $"- {p.Value} → {p.Key}").Join('\n')}");
         foreach (var pair in sources) archive.CreateEntryFromFile(PathUtils.WithLongPath(pair.Value), pair.Key.Replace('\\', '/'), CompressionLevel.Optimal);
     }

+ 49 - 0
Shared/Utils/ResilientUtils.cs

@@ -0,0 +1,49 @@
+namespace MeloongCore;
+public static class ResilientUtils {
+
+    /// <summary>
+    /// 在抛出异常时,延迟并自动重试。
+    /// </summary>
+    public static void Retry(Action action, int maxAttempts = 2, int delayMs = 200, [CallerMemberName] string caller = "")
+        => RetryOn<Exception>(action, maxAttempts, delayMs, caller);
+    /// <summary>
+    /// 在抛出特定异常时,延迟并自动重试。
+    /// </summary>
+    public static void RetryOn<TException>(Action action, int maxAttempts = 2, int delayMs = 200, [CallerMemberName] string caller = "") where TException : Exception {
+        int attempt = 0;
+        while (true) {
+            try {
+                action();
+                return; // 成功则退出
+            } catch (TException ex) {
+                attempt++;
+                if (attempt >= maxAttempts) throw; // 超过最大尝试次数
+                Logger.Warn(ex, $"{caller} 第 {attempt} 次尝试失败,将在 {delayMs}ms 后重试");
+                Thread.Sleep(delayMs);
+            }
+        }
+    }
+
+    /// <summary>
+    /// 在抛出异常时,延迟并自动重试。
+    /// </summary>
+    public static void Retry<TOut>(Func<TOut> func, int maxAttempts = 2, int delayMs = 200, [CallerMemberName] string caller = "")
+        => RetryOn<Exception, TOut>(func, maxAttempts, delayMs, caller);
+    /// <summary>
+    /// 在抛出特定异常时,延迟并自动重试。
+    /// </summary>
+    public static TOut RetryOn<TException, TOut>(Func<TOut> func, int maxAttempts = 2, int delayMs = 200, [CallerMemberName] string caller = "") where TException : Exception {
+        int attempt = 0;
+        while (true) {
+            try {
+                return func(); // 成功则退出
+            } catch (TException ex) {
+                attempt++;
+                if (attempt >= maxAttempts) throw; // 超过最大尝试次数
+                Logger.Warn(ex, $"{caller} 第 {attempt} 次尝试失败,将在 {delayMs}ms 后重试");
+                Thread.Sleep(delayMs);
+            }
+        }
+    }
+
+}