From 26f4706247cc055a4f67796752a3bc9d141d0f10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:38:58 +0000 Subject: [PATCH 1/6] Initial plan From 9c3dee096370bc4a3790f28691f8039584fb0ee4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:41:24 +0000 Subject: [PATCH 2/6] Fix file occupation exception by using File.Move with File.Copy fallback Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Binary/BinaryHandler.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs index a9eafb40..336b866d 100644 --- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs +++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs @@ -363,16 +363,29 @@ await Task.Run(() => } } + // Remove old file if it exists if (File.Exists(_oldfilePath)) { File.SetAttributes(_oldfilePath, FileAttributes.Normal); File.Delete(_oldfilePath); } + // Try to move the new file to the old file location + // If move fails due to file occupation (race condition), fall back to copy if (File.Exists(_newfilePath)) { File.SetAttributes(_newfilePath, FileAttributes.Normal); - File.Copy(_newfilePath, _oldfilePath, true); + try + { + // Try File.Move first as it's more efficient + File.Move(_newfilePath, _oldfilePath); + } + catch (IOException) + { + // If move fails due to file occupation or other IO issues, + // fall back to copy which is more resilient + File.Copy(_newfilePath, _oldfilePath, true); + } } }); } From 9e60aef249b73fac58c18b4017a7d834038a5361 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:43:23 +0000 Subject: [PATCH 3/6] Delete source file after File.Copy to maintain consistency with File.Move behavior Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs index 336b866d..2e544d3e 100644 --- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs +++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs @@ -385,6 +385,8 @@ await Task.Run(() => // If move fails due to file occupation or other IO issues, // fall back to copy which is more resilient File.Copy(_newfilePath, _oldfilePath, true); + // Delete the source file to maintain consistency with File.Move behavior + File.Delete(_newfilePath); } } }); From 2d91b0f8293074abc3b70c3c1b30a4b5216532aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:54:52 +0000 Subject: [PATCH 4/6] Address root cause: Force GC and add retry logic with exponential backoff to handle file handle release delays Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Binary/BinaryHandler.cs | 91 +++++++++++++------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs index 2e544d3e..f8509be6 100644 --- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs +++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs @@ -229,13 +229,31 @@ await Task.Run(() => _newfilePath = newfilePath; _patchPath = patchPath; ValidationParameters(); - using (FileStream input = - new FileStream(_oldfilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + + // Apply the patch to create the new file + 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 + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // Replace the old file with the patched new file + ReplaceOldFileWithNew(); + }); + } + + private void ApplyPatch() + { + 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,36 +378,55 @@ await Task.Run(() => } } } - } + // Explicitly flush and close the output stream + output.Flush(); } + // Both streams are now disposed and closed + } + } - // Remove old file if it exists - if (File.Exists(_oldfilePath)) - { - File.SetAttributes(_oldfilePath, FileAttributes.Normal); - File.Delete(_oldfilePath); - } - - // Try to move the new file to the old file location - // If move fails due to file occupation (race condition), fall back to copy - if (File.Exists(_newfilePath)) + private void ReplaceOldFileWithNew() + { + // 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(_newfilePath, FileAttributes.Normal); - try + if (File.Exists(_oldfilePath)) { - // Try File.Move first as it's more efficient - File.Move(_newfilePath, _oldfilePath); + File.SetAttributes(_oldfilePath, FileAttributes.Normal); + File.Delete(_oldfilePath); } - catch (IOException) + + if (File.Exists(_newfilePath)) { - // If move fails due to file occupation or other IO issues, - // fall back to copy which is more resilient + File.SetAttributes(_newfilePath, FileAttributes.Normal); File.Copy(_newfilePath, _oldfilePath, true); - // Delete the source file to maintain consistency with File.Move behavior - File.Delete(_newfilePath); } + + // Success - exit the retry loop + return; } - }); + catch (IOException ex) when (attempt < maxRetries) + { + // File is still locked - wait and retry + // Use exponential backoff: 50ms, 100ms, 200ms + System.Threading.Thread.Sleep(retryDelayMs); + retryDelayMs *= 2; + + // Force another garbage collection to release any remaining handles + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + // On the last attempt, let the exception bubble up + } } #endregion Public Methods From 57a5ea0b9123f8ef1c5c535766043776937b049c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:57:24 +0000 Subject: [PATCH 5/6] Address code review feedback: delete temp file, use async Task.Delay, remove unused variable Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Binary/BinaryHandler.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs index f8509be6..2596e679 100644 --- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs +++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs @@ -223,7 +223,7 @@ 0 32 Header /// public async Task Dirty(string oldfilePath, string newfilePath, string patchPath) { - await Task.Run(() => + await Task.Run(async () => { _oldfilePath = oldfilePath; _newfilePath = newfilePath; @@ -241,7 +241,7 @@ await Task.Run(() => GC.Collect(); // Replace the old file with the patched new file - ReplaceOldFileWithNew(); + await ReplaceOldFileWithNewAsync(); }); } @@ -385,7 +385,7 @@ private void ApplyPatch() } } - private void ReplaceOldFileWithNew() + 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, @@ -409,16 +409,18 @@ private void ReplaceOldFileWithNew() { 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; } - catch (IOException ex) when (attempt < maxRetries) + catch (IOException) when (attempt < maxRetries) { // File is still locked - wait and retry // Use exponential backoff: 50ms, 100ms, 200ms - System.Threading.Thread.Sleep(retryDelayMs); + await Task.Delay(retryDelayMs); retryDelayMs *= 2; // Force another garbage collection to release any remaining handles From d5e5feb65ecd133b4023c7ed8bb70dc570fadbfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:59:52 +0000 Subject: [PATCH 6/6] Remove unnecessary Task.Run wrapper and GC calls from retry loop, clarify comments Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com> --- .../Binary/BinaryHandler.cs | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs index 2596e679..7240d9ad 100644 --- a/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs +++ b/src/c#/GeneralUpdate.Differential/Binary/BinaryHandler.cs @@ -223,26 +223,25 @@ 0 32 Header /// public async Task Dirty(string oldfilePath, string newfilePath, string patchPath) { - await Task.Run(async () => - { - _oldfilePath = oldfilePath; - _newfilePath = newfilePath; - _patchPath = patchPath; - ValidationParameters(); - - // Apply the patch to create the new file - 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 - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - - // Replace the old file with the patched new file - await ReplaceOldFileWithNewAsync(); - }); + _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() @@ -422,12 +421,9 @@ private async Task ReplaceOldFileWithNewAsync() // Use exponential backoff: 50ms, 100ms, 200ms await Task.Delay(retryDelayMs); retryDelayMs *= 2; - - // Force another garbage collection to release any remaining handles - GC.Collect(); - GC.WaitForPendingFinalizers(); } - // On the last attempt, let the exception bubble up + // On the final attempt (when attempt == maxRetries), IOException will not be caught + // and will bubble up to the caller } }