diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index f7ae52d..2640a7e 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "0.28.10" +#define MyAppVersion "0.28.11" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 4d65a25..241b974 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -18,9 +18,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.28.10 - 0.28.10.0 - 0.28.10.0 + 0.28.11 + 0.28.11.0 + 0.28.11.0 true diff --git a/src/PSLauncher.Core/Downloads/DownloadManager.cs b/src/PSLauncher.Core/Downloads/DownloadManager.cs index a5696a7..5aa35d0 100644 --- a/src/PSLauncher.Core/Downloads/DownloadManager.cs +++ b/src/PSLauncher.Core/Downloads/DownloadManager.cs @@ -298,7 +298,12 @@ public sealed class DownloadManager : IDownloadManager // 4. Lance les workers en parallèle. // Compteur agrégé via Interlocked → pas de lock dans le hot path. - long aggregateBytes = state.DownloadedBytes; + // Initialisé sur la somme des seg.DownloadedBytes DURABLES plutôt que sur + // state.DownloadedBytes (live aggregate persisté, qui peut être en avance + // de la réalité disque si pause survenue entre checkpoints). Sans ça, le + // footer afficherait "paused at X" mais segments resumeraient à un point + // en arrière, et l'aggregate finirait avec un drift cosmétique. + long aggregateBytes = state.Segments.Sum(s => s.DownloadedBytes); // Action passée aux segments : juste un Add atomique (zéro contention). void OnSegmentBytes(int _, long deltaBytes) @@ -474,15 +479,46 @@ public sealed class DownloadManager : IDownloadManager await using var dst = new FileStream(partialPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, BufferSize, useAsync: true); dst.Seek(segStart, SeekOrigin.Begin); + // DURABILITÉ : on n'incrémente seg.DownloadedBytes qu'APRÈS un FlushAsync + // réussi. Sans ça, sur pause/cancel, le buffer FileStream (4 MiB) ou le + // cache OS peuvent ne pas avoir atteint le disque, mais seg.DownloadedBytes + // les compte comme écrits → resume saute ces bytes → trou (zéros NTFS du + // pre-alloc sparse) → SHA-256 fail. Cas observé chez l'utilisateur : + // ~80% des reprises post-pause failaient la vérif hash. + // + // Stratégie : tracker en local le delta non-flushé. Tous les ~64 MiB, + // FlushAsync (forcé avec CancellationToken.None pour qu'une pause ne le + // coupe pas en plein milieu) puis update seg.DownloadedBytes. Le compteur + // est donc TOUJOURS en arrière (ou égal) de ce qui est sur disque, jamais + // en avance. Resume re-télécharge au pire 64 MiB par segment — coût + // borné, et c'est ce qu'on appelle un checkpoint. + const long FlushIntervalBytes = 64L * 1024 * 1024; var buffer = new byte[BufferSize]; + long inFlight = 0; int n; while ((n = await src.ReadAsync(buffer.AsMemory(0, BufferSize), ct).ConfigureAwait(false)) > 0) { await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false); - seg.DownloadedBytes += n; + inFlight += n; + // Live counter pour UI (aggregate / footer) — peut overshoot par + // rapport au durable de quelques MiB ; le footer revient cohérent + // au prochain checkpoint. onBytes(seg.Index, n); + + if (inFlight >= FlushIntervalBytes) + { + await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false); + seg.DownloadedBytes += inFlight; + inFlight = 0; + } + } + // Checkpoint final : flush + ack du reste du buffer. + await dst.FlushAsync(CancellationToken.None).ConfigureAwait(false); + if (inFlight > 0) + { + seg.DownloadedBytes += inFlight; + inFlight = 0; } - await dst.FlushAsync(ct).ConfigureAwait(false); seg.Completed = (seg.DownloadedBytes >= seg.Length); // Si on est sorti de la boucle sans avoir atteint la fin du segment, le serveur a