Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 70 additions & 20 deletions src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,36 @@ 0 32 Header
/// <exception cref="InvalidOperationException"></exception>
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<Stream> openPatchStream = () =>
new FileStream(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read);
Func<Stream> openPatchStream = () =>
new FileStream(_patchPath, FileMode.Open, FileAccess.Read, FileShare.Read);
//File format:
// 0 8 "BSDIFF40"
// 8 8 X
Expand Down Expand Up @@ -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
Expand Down