diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 2640a7e..1639d5a 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.11" +#define MyAppVersion "0.28.12" #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 241b974..2ce162d 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.11 - 0.28.11.0 - 0.28.11.0 + 0.28.12 + 0.28.12.0 + 0.28.12.0 true diff --git a/src/PSLauncher.Core/Downloads/DownloadManager.cs b/src/PSLauncher.Core/Downloads/DownloadManager.cs index 5aa35d0..23b373c 100644 --- a/src/PSLauncher.Core/Downloads/DownloadManager.cs +++ b/src/PSLauncher.Core/Downloads/DownloadManager.cs @@ -479,19 +479,26 @@ 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 + // CHECKPOINT : durabilité ET anti-double-comptage. + // + // 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. + // pre-alloc sparse) → SHA-256 fail. // - // 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. + // Anti-double-comptage : on appelle ÉGALEMENT `onBytes` uniquement au + // checkpoint, avec le delta inFlight. Sans ça, un retry Polly mid-segment + // (très fréquent sur PHP-FPM OVH mutualisé qui coupe les requêtes longues) + // re-télécharge les bytes depuis le dernier checkpoint et les compte une + // 2e fois dans l'aggregate → footer affiche >100% en fin de DL. + // Avec onBytes au checkpoint : les bytes re-téléchargés n'ont jamais été + // reportés à la 1re tentative (l'erreur Polly est levée AVANT le checkpoint + // sur l'attempt failed), donc pas de double count. + // + // Trade-off : UI/footer update tous les 64 MiB par segment au lieu de + // chaque 4 MiB. Avec 16 segments en parallèle, ça reste ~10-20 updates/s + // au pic, le reporter task échantillonne à 4 Hz donc invisible côté UX. const long FlushIntervalBytes = 64L * 1024 * 1024; var buffer = new byte[BufferSize]; long inFlight = 0; @@ -500,15 +507,12 @@ public sealed class DownloadManager : IDownloadManager { await dst.WriteAsync(buffer.AsMemory(0, n), ct).ConfigureAwait(false); 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; + onBytes(seg.Index, inFlight); inFlight = 0; } } @@ -517,6 +521,7 @@ public sealed class DownloadManager : IDownloadManager if (inFlight > 0) { seg.DownloadedBytes += inFlight; + onBytes(seg.Index, inFlight); inFlight = 0; } seg.Completed = (seg.DownloadedBytes >= seg.Length);