Bug rapporté : ~80% des reprises après pause failaient la vérification
SHA-256 finale alors que la DL semblait s'être passée correctement et
que le fichier était de la bonne taille. Root cause : seg.DownloadedBytes
était incrémenté juste après le retour de WriteAsync, mais cet appel met
seulement les bytes dans le buffer interne du FileStream (4 MiB) — pas
forcément sur disque ni dans le cache OS.
À la pause, le ct est cancelé, le segment throw une OperationCanceledException
au prochain await, et `await using var dst` dispose le FileStream — qui
SENSE flush le buffer vers le disque mais avec useAsync=true + cancellation
en cours, ce flush peut être partiel ou échouer silencieusement. Résultat :
le compteur dit "X bytes écrits" mais le disque en a moins. Au resume,
segStart = seg.Start + X évite donc les bytes perdus, ces régions
restent en zéros sparse (pré-alloc), et le SHA final fail.
Fix : pattern checkpoint. Le compteur seg.DownloadedBytes n'est mis à
jour qu'APRÈS un FlushAsync(CancellationToken.None) réussi. Le flush
est forcé non-cancelable pour éviter qu'une pause l'interrompe en
plein milieu. Granularité : tous les 64 MiB de download par segment.
Invariant garanti après le fix :
seg.DownloadedBytes <= bytes_réellement_sur_disque
Au pire, en cas de pause, seg.DownloadedBytes est UN PEU en arrière de
ce qui est sur disque (jusqu'à 64 MiB par segment). Au resume, on
re-télécharge ces bytes — ils sont écrasés avec le même contenu, le
SHA final passe. Le cas dangereux ("compteur en avance, trou de zéros
sur disque") est définitivement impossible.
Bonus : initialisation de aggregateBytes depuis sum(seg.DownloadedBytes)
au lieu de state.DownloadedBytes pour éviter un drift cosmétique du
footer après resume (state.DownloadedBytes capturait la live aggregate
incluant l'in-flight, donc pouvait dépasser le total durable).
Trade-off : un peu plus de re-download au resume (jusqu'à 1 GB pour
16 segments × 64 MiB) en échange d'une fiabilité totale. Le user ne
verra pas la différence sur un 14 GB de DL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PS_Launcher — Installeur
Génère un setup Windows (PSLauncher-Setup-{version}.exe) qui distribue
le launcher principal + son updater dans Program Files\ASTERION VR\PSLauncher\.
Pré-requis
- .NET 8 SDK (déjà installé pour build le projet)
- Inno Setup 6.x — https://jrsoftware.org/isdl.php (gratuit, ~6 Mo)
Build en une commande
Depuis la racine du repo :
powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1
Ça enchaîne :
dotnet publish src/PSLauncher.App -c Release→PSLauncher.exe(~77 Mo)dotnet publish src/PSLauncher.Updater -c Release→PSLauncher.Updater.exe(~10 Mo)ISCC PSLauncher.iss→installer/output/PSLauncher-Setup-X.Y.Z.exe
Le fichier final fait ~80 Mo (compression ultra Inno + binaires self-contained).
Distribution
Tu balances un seul .exe au client :
- Il lance, choisit français/anglais, clique Suivant×3, c'est installé.
- Raccourci bureau + menu Démarrer créés.
- Désinstallation propre via Programmes et fonctionnalités.
Le cache utilisateur (%LocalAppData%\PSLauncher\config.json, logs, downloads partiels)
n'est pas touché par l'install/désinstall — la license et les paramètres survivent.
Mise à jour du launcher
Le launcher se met à jour seul via PSLauncher.Updater.exe (cf. flow auto-update).
Le client n'a pas besoin de rejouer l'installeur sauf changement majeur (TFM, déps).
Bumper la version
Avant de rebuild un installeur :
src/PSLauncher.App/PSLauncher.App.csproj→<Version>X.Y.Z</Version>(et AssemblyVersion / FileVersion)installer/PSLauncher.iss→#define MyAppVersion "X.Y.Z"installer/build-installer.ps1
Côté serveur, mets aussi à jour manifest/versions.json section launcher avec la
nouvelle version + URL de download du nouveau PSLauncher.exe (via le backoffice
ou édition manuelle, puis php tools/sign-manifest.php).
Code-signing (optionnel)
Pour éviter le warning SmartScreen "éditeur inconnu", il faut signer
PSLauncher.exe, PSLauncher.Updater.exe et PSLauncher-Setup-X.Y.Z.exe avec
un certificat Authenticode (OV ~250 €/an, EV ~400 €/an, ou gratuit via SignPath
pour OSS). Hors-scope du script actuel.