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