From c27f690ff3947733d4b6f08f8feb932a4f3fb3a6 Mon Sep 17 00:00:00 2001 From: FaulMit <48646918+FaulMit@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:32:44 +0200 Subject: [PATCH] Fix Windows update file lock --- CHANGELOG.md | 10 +++ src/Captail/Captail.csproj | 2 +- src/Captail/UpdateService.cs | 135 +++++++++++++++++++---------------- 3 files changed, 83 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2925937..5ac1cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable user-facing changes are documented here. ## [Unreleased] +## [0.1.5] - 2026-08-02 + +### Fixed + +- Update downloads now close their file handles before promoting verified packages or replacing an invalid cached package, preventing Windows file-lock errors during one-click updates. + +### Upgrade note + +- Captail 0.1.3 and 0.1.4 cannot complete an in-app update because the bug is inside those installed versions. Download and run the 0.1.5 Setup EXE once; later in-app updates will work normally. + ## [0.1.3] - 2026-07-26 ### Added diff --git a/src/Captail/Captail.csproj b/src/Captail/Captail.csproj index fbd530c..111a60e 100644 --- a/src/Captail/Captail.csproj +++ b/src/Captail/Captail.csproj @@ -14,7 +14,7 @@ Captail Captail Assets\Captail.ico - 0.1.4 + 0.1.5 true diff --git a/src/Captail/UpdateService.cs b/src/Captail/UpdateService.cs index 39bc664..89ebeb4 100644 --- a/src/Captail/UpdateService.cs +++ b/src/Captail/UpdateService.cs @@ -422,16 +422,21 @@ private static async Task DownloadVerifiedAsync( if (File.Exists(destinationPath) && new FileInfo(destinationPath).Length == asset.Size) { - await using var cachedFile = new FileStream( - destinationPath, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - 128 * 1024, - FileOptions.Asynchronous | FileOptions.SequentialScan); - byte[] cachedHash = await SHA256.HashDataAsync( - cachedFile, - cancellationToken); + byte[] cachedHash; + await using (var cachedFile = new FileStream( + destinationPath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 128 * 1024, + FileOptions.Asynchronous | + FileOptions.SequentialScan)) + { + cachedHash = await SHA256.HashDataAsync( + cachedFile, + cancellationToken); + } + if (CryptographicOperations.FixedTimeEquals( Encoding.ASCII.GetBytes( Convert.ToHexString(cachedHash) @@ -460,66 +465,70 @@ private static async Task DownloadVerifiedAsync( "Downloaded update size does not match release metadata."); } - await using Stream source = - await response.Content.ReadAsStreamAsync(cancellationToken); - await using var destination = new FileStream( - temporaryPath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - 128 * 1024, - FileOptions.Asynchronous | FileOptions.SequentialScan); - using IncrementalHash hash = - IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - - byte[] buffer = new byte[128 * 1024]; - long total = 0; - int lastProgress = -1; - while (true) + await using (Stream source = + await response.Content.ReadAsStreamAsync( + cancellationToken)) + await using (var destination = new FileStream( + temporaryPath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + 128 * 1024, + FileOptions.Asynchronous | + FileOptions.SequentialScan)) { - int read = await source.ReadAsync( - buffer, - cancellationToken); - if (read == 0) - break; - total += read; - if (total > asset.Size || total > MaximumAssetBytes) + using IncrementalHash hash = + IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + + byte[] buffer = new byte[128 * 1024]; + long total = 0; + int lastProgress = -1; + while (true) + { + int read = await source.ReadAsync( + buffer, + cancellationToken); + if (read == 0) + break; + total += read; + if (total > asset.Size || total > MaximumAssetBytes) + { + throw new InvalidDataException( + "Downloaded update exceeds expected size."); + } + hash.AppendData(buffer, 0, read); + await destination.WriteAsync( + buffer.AsMemory(0, read), + cancellationToken); + + int percent = (int)Math.Clamp( + total * 100L / asset.Size, + 0, + 100); + if (percent != lastProgress) + { + progress?.Report(percent); + lastProgress = percent; + } + } + await destination.FlushAsync(cancellationToken); + + if (total != asset.Size) { throw new InvalidDataException( - "Downloaded update exceeds expected size."); + "Downloaded update is incomplete."); } - hash.AppendData(buffer, 0, read); - await destination.WriteAsync( - buffer.AsMemory(0, read), - cancellationToken); - - int percent = (int)Math.Clamp( - total * 100L / asset.Size, - 0, - 100); - if (percent != lastProgress) + string actualHash = + Convert.ToHexString(hash.GetHashAndReset()) + .ToLowerInvariant(); + if (!CryptographicOperations.FixedTimeEquals( + Encoding.ASCII.GetBytes(actualHash), + Encoding.ASCII.GetBytes(expectedHash))) { - progress?.Report(percent); - lastProgress = percent; + throw new InvalidDataException( + "Downloaded update failed SHA-256 verification."); } } - await destination.FlushAsync(cancellationToken); - - if (total != asset.Size) - { - throw new InvalidDataException( - "Downloaded update is incomplete."); - } - string actualHash = - Convert.ToHexString(hash.GetHashAndReset()) - .ToLowerInvariant(); - if (!CryptographicOperations.FixedTimeEquals( - Encoding.ASCII.GetBytes(actualHash), - Encoding.ASCII.GetBytes(expectedHash))) - { - throw new InvalidDataException( - "Downloaded update failed SHA-256 verification."); - } File.Move(temporaryPath, destinationPath, overwrite: true); progress?.Report(100);