diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs
index a9eafb40..7240d9ad 100644
--- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs
+++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs
@@ -223,19 +223,36 @@ 0 32 Header
///
public async Task Dirty(string oldfilePath, string newfilePath, string patchPath)
{
- await Task.Run(() =>
+ _oldfilePath = oldfilePath;
+ _newfilePath = newfilePath;
+ _patchPath = patchPath;
+ ValidationParameters();
+
+ // Apply the patch to create the new file (CPU-intensive work on thread pool)
+ await Task.Run(() => ApplyPatch());
+
+ // Force finalization to ensure file handles are released
+ // This addresses the root cause: file handles may not be immediately released
+ // after disposal, especially in multi-threaded environments or during abnormal shutdowns
+ // Note: While explicit GC calls should generally be avoided, this is necessary here
+ // to ensure OS-level file handles are released before attempting file operations
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ // Replace the old file with the patched new file
+ await ReplaceOldFileWithNewAsync();
+ }
+
+ private void ApplyPatch()
+ {
+ using (FileStream input =
+ new FileStream(_oldfilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
- _oldfilePath = oldfilePath;
- _newfilePath = newfilePath;
- _patchPath = patchPath;
- ValidationParameters();
- using (FileStream input =
- new FileStream(_oldfilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (FileStream output = new FileStream(_newfilePath, FileMode.Create))
{
- using (FileStream output = new FileStream(_newfilePath, FileMode.Create))
- {
- Func openPatchStream = () =>
- new FileStream(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ Func openPatchStream = () =>
+ new FileStream(_patchPath, FileMode.Open, FileAccess.Read, FileShare.Read);
//File format:
// 0 8 "BSDIFF40"
// 8 8 X
@@ -360,21 +377,54 @@ await Task.Run(() =>
}
}
}
- }
+ // Explicitly flush and close the output stream
+ output.Flush();
}
+ // Both streams are now disposed and closed
+ }
+ }
- if (File.Exists(_oldfilePath))
+ private async Task ReplaceOldFileWithNewAsync()
+ {
+ // At this point, all file handles should be released due to explicit disposal
+ // and forced garbage collection. However, the OS may still hold handles briefly,
+ // especially in multi-threaded scenarios or abnormal shutdowns.
+
+ // Use retry logic with exponential backoff to handle transient file locks
+ int maxRetries = 3;
+ int retryDelayMs = 50;
+
+ for (int attempt = 0; attempt <= maxRetries; attempt++)
+ {
+ try
{
- File.SetAttributes(_oldfilePath, FileAttributes.Normal);
- File.Delete(_oldfilePath);
+ if (File.Exists(_oldfilePath))
+ {
+ File.SetAttributes(_oldfilePath, FileAttributes.Normal);
+ File.Delete(_oldfilePath);
+ }
+
+ if (File.Exists(_newfilePath))
+ {
+ File.SetAttributes(_newfilePath, FileAttributes.Normal);
+ File.Copy(_newfilePath, _oldfilePath, true);
+ // Delete the temporary file after successful copy
+ File.Delete(_newfilePath);
+ }
+
+ // Success - exit the retry loop
+ return;
}
-
- if (File.Exists(_newfilePath))
+ catch (IOException) when (attempt < maxRetries)
{
- File.SetAttributes(_newfilePath, FileAttributes.Normal);
- File.Copy(_newfilePath, _oldfilePath, true);
+ // File is still locked - wait and retry
+ // Use exponential backoff: 50ms, 100ms, 200ms
+ await Task.Delay(retryDelayMs);
+ retryDelayMs *= 2;
}
- });
+ // On the final attempt (when attempt == maxRetries), IOException will not be caught
+ // and will bubble up to the caller
+ }
}
#endregion Public Methods