v0.29.7 — SteamVR merge + résilience DL (404 retry, force fresh, SHA purge)
SteamVR + Vive Business Streaming settings merge :
- Nouveau ISteamVrSettingsDeployer : si _steamvr/steamvr.vrsettings est
dans le ZIP, deep-merge récursif des blocs racine dans le fichier user
(clé absente → ajout, deux objets → recurse, sinon → replace ; les
sous-clés cible non touchées sont préservées — crucial pour "trackers")
- Auto-localisation via registre Steam (HKCU/HKLM) + fallback Program Files
- Phase pré-check (CheckMergeNeededAsync) : skip silencieux si la cible
est déjà à jour (deep equality) → pas de kill SteamVR/VBS inutile, pas
de popup à l'opérateur. Si changements nécessaires → popup OK/Annuler
avec nombre de blocs qui changeront + liste des process à fermer.
- Liste de kill construite depuis les health checks de type "Process"
(l'opérateur connaît déjà sa stack VR) + guardians (Vive Business
Streaming en tête car il relance SteamVR auto)
- Settings UI : section dédiée avec opt-in, override path, liste process
éditable + affichage du path canonique attendu
Résilience téléchargements :
- 404 mid-DL traité comme 403/410 (URL refresh trigger) au niveau segment :
on extrait le nouveau filename via /download-url server-side, retry transparent
- Auto-retry install une fois après refresh manifest sur 404 : l'opérateur
ne voit rien si la cause était un manifest local stale
- Si retry échoue aussi en 404 → message ciblé "manifest serveur stale"
(problème côté serveur, pas client)
- Sur SHA-256 mismatch : auto-purge du cache LAN local (.zip + .sha256)
+ message d'erreur dédié avec source du DL (peer ou OVH) + nouveau menu
"↻ Forcer re-téléchargement" pour purger état + cache manuellement
- Détection client-side de l'incohérence "signed URL filename != manifest
URL filename" avant DL : abort immédiat plutôt que 14 Go pour rien
- IZipCacheStore.InvalidateAsync : nouvelle API pour purger un cache par
version (utilisée par SHA mismatch handler + Force Fresh menu)
Bug serveur (DownloadUrl.php) :
- L'endpoint construisait l'URL signée avec un template hardcodé
`/builds/proserve-{version}.zip`, ignorant complètement
download.url du manifest. Conséquence : un opérateur qui rename
son ZIP pour buster le cache CDN OVH (ex. proserve-full-1.5.4.zip)
voyait toutes ses releases retournent du 404 silencieux côté client.
- Fix : on lit basename(parse_url(entry.download.url).path), whitelist
sur le filename, vérif is_file() avant de signer, erreur 500 explicite
si manifest et filesystem désynchros.
Strings (5 langues) :
- ~20 nouvelles : SteamVR popups + status, SHA mismatch dialog + source
labels, force fresh confirm + menu, manifest stale + auto-retry status
Bumps : 0.29.6 (déployé hors-commit pendant la session) → 0.29.7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -455,16 +455,52 @@ public sealed class DownloadManager : IDownloadManager
|
||||
throw new HttpResumableException("Server returned 200 for segment range request", isTransient: true);
|
||||
}
|
||||
|
||||
// 403 / 410 = URL HMAC expirée (gate.php renvoie 403 "Forbidden: expired").
|
||||
// On force un refresh d'URL via le callback du job, puis on jette une
|
||||
// exception transitoire pour que Polly retry avec la nouvelle URL.
|
||||
if (resp.StatusCode == HttpStatusCode.Forbidden || resp.StatusCode == HttpStatusCode.Gone)
|
||||
// 403 / 410 / 404 = URL HMAC expirée (gate.php renvoie 403 "Forbidden:
|
||||
// expired"), OU le fichier a été renommé/supprimé côté serveur (cas
|
||||
// typique : ré-upload du ZIP avec un nouveau nom pour invalider le cache
|
||||
// OVH CDN). Dans les deux cas, refresh l'URL via le callback du job —
|
||||
// l'endpoint /download-url/{version} renvoie la NOUVELLE URL pointant
|
||||
// vers le fichier actuellement présent sur le serveur. Si refresh donne
|
||||
// une URL DIFFÉRENTE de celle qu'on a utilisée dans la requête → retry
|
||||
// avec la nouvelle. Sinon → c'est un vrai 404, on bubble.
|
||||
//
|
||||
// Note importante : on compare à `url` (la valeur utilisée dans la
|
||||
// requête HTTP, capturée ligne 438 avant le send) plutôt qu'à un
|
||||
// snapshot pris juste avant forceRefresh. Pourquoi : si 8 segments
|
||||
// tombent en 404 simultanément, le PREMIER refresh met _currentUrl à
|
||||
// jour, les autres voient le debounce 5s (no-op). Comparer à `url`
|
||||
// (= URL réellement utilisée par CETTE requête, OLD) détecte
|
||||
// correctement que _currentUrl est maintenant NEW → retry. Si on
|
||||
// comparait à un snapshot pré-refresh, on raterait ce cas et on jetterait
|
||||
// un faux 404 fatal sur tous les segments sauf le premier.
|
||||
bool isUrlRefreshTrigger = resp.StatusCode == HttpStatusCode.Forbidden
|
||||
|| resp.StatusCode == HttpStatusCode.Gone
|
||||
|| resp.StatusCode == HttpStatusCode.NotFound;
|
||||
if (isUrlRefreshTrigger && job.RefreshUrlAsync is not null)
|
||||
{
|
||||
if (job.RefreshUrlAsync is not null)
|
||||
await GetOrRefreshUrlAsync(job, forceRefresh: true, ct).ConfigureAwait(false);
|
||||
bool urlChanged = !Uri.Equals(url, _currentUrl);
|
||||
if (urlChanged)
|
||||
{
|
||||
await GetOrRefreshUrlAsync(job, forceRefresh: true, ct).ConfigureAwait(false);
|
||||
throw new HttpResumableException("Signed URL expired, refreshed for next try", isTransient: true);
|
||||
_logger.LogInformation(
|
||||
"Got HTTP {Status} on segment {Seg}, URL refreshed (old={Old} → new={New}), retrying",
|
||||
(int)resp.StatusCode, seg.Index, url, _currentUrl);
|
||||
throw new HttpResumableException(
|
||||
$"HTTP {(int)resp.StatusCode} on segment {seg.Index}, URL refreshed",
|
||||
isTransient: true);
|
||||
}
|
||||
// URL unchanged → soit le serveur insiste sur la même URL morte,
|
||||
// soit le refresh a fallback sur le manifest cached (donc stale).
|
||||
// 404 non-transient pour arrêter Polly et remonter une erreur claire
|
||||
// qui guide l'opérateur vers « Vérifier les MAJ ».
|
||||
if (resp.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new HttpResumableException(
|
||||
$"HTTP 404 on segment {seg.Index} — le fichier a été renommé/supprimé côté serveur et le manifest local est obsolète. Clique « Vérifier les MAJ » avant de réessayer.",
|
||||
isTransient: false);
|
||||
}
|
||||
throw new HttpResumableException(
|
||||
$"Signed URL expired (HTTP {(int)resp.StatusCode}), refreshed for next try", isTransient: true);
|
||||
}
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user