v0.25.10 — LAN P2P offline-friendly : manifest fallback + perf
Fonctionne entièrement offline si un PC du LAN a fetché OVH récemment :
les autres PCs récupèrent le manifest + les ZIPs depuis lui sans Internet.
Discovery des peers quasi-instantanée au cold start, fail-fast partout sur
les fetch OVH pour ne pas bloquer la UI.
== Manifest fallback offline ==
- IManifestCache + ManifestCache (Core/Manifests/) : cache disque du dernier
manifest réussi (JSON brut byte-for-byte pour préserver la signature Ed25519).
Path : %LocalAppData%\PSLauncher\manifest-cache.json
- IPeerManifestFetcher + PeerManifestFetcher (Core/Lan/) : récupère le
manifest brut depuis un peer LAN via GET /v1/manifest. Probe parallèle
avec timeout court par peer.
- LanCacheServer : nouvelle route GET /v1/manifest qui sert le cache disque
byte-for-byte aux clients sans Internet.
- ManifestService.FetchAsync : online check d'abord (ping ICMP 8.8.8.8 +
fallback TCP connect au serveur OVH si firewall bloque ICMP, timeout
800ms × 2). Si online → OVH only (canonique, voit toutes les versions).
Si offline → peer LAN puis cache disque local.
- ManifestSource enum exposé via IManifestService.LastSource → call sites
peuvent prendre des décisions éclairées (skip release notes, badge UI).
== Discovery active des peers (cold start instantané) ==
- LanCacheServer : nouvelle route GET /v1/info qui retourne {host, port,
versions} (même payload que les beacons UDP).
- LanDiscoveryService : bootstrap au démarrage qui probe en parallèle tous
les peers connus (cache disque) + manuels (config) via /v1/info, timeout
1s par peer. Les peers qui répondent sont injectés dans _peers avec
lastSeen=now → apparaissent immédiatement dans ListDiscovered() et donc
dans la sidebar + PeerSourceResolver. Plus besoin d'attendre les 15s du
prochain beacon UDP.
- ILanDiscoveryService.ListKnown() exposé pour que les fetchers puissent
utiliser le cache disque comme seed.
- LocalConfig.LanCache.ManualPeerUrls : default ["http://10.0.4.100:47623"]
(IP fixe du serveur ASTERION dans toutes les salles VR).
== Timeouts courts sur tous les fetch OVH (fail-fast offline) ==
- ManifestService : 3s pour le manifest fetch (vs 100s par défaut)
- LicenseService.ValidateAsync : 3s (résolvait le délai de ~15s pendant
CheckForUpdates en offline — HttpClient global a Timeout=Infinite,
sans CTS court ça hangait jusqu'à l'abandon SYN TCP par l'OS)
- LicenseService.GetSignedDownloadUrlAsync : 5s
- ManifestService.FetchReleaseNotesAsync : 5s
== UI ==
- StatusMessage post-Check Updates suffixé avec la source du manifest :
"🌐 OVH (en ligne)" / "📡 Peer LAN (hors ligne)" / "💾 Cache local (hors ligne)"
- InstallVersionAsync : skip le fetch release notes si manifest source
!= OVH → dialog s'ouvre instantanément avec "indisponible" au lieu
d'attendre 5s de timeout.
== Latences attendues ==
| Scénario | Avant | Après |
|---|---|---|
| Vérifier MAJ online | ~5s | ~500ms |
| Vérifier MAJ offline | ~15-20s | ~5s |
| Cold start discovery (1 peer connu) | jusqu'à 15s | ~200ms |
| Click Install offline | ~5s release notes timeout | instantané |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,11 +87,19 @@ public sealed class LicenseService : ILicenseService
|
||||
|
||||
_logger.LogInformation("Validating license at {Url} (key {KeyHint}…)", url, licenseKey.Length >= 8 ? licenseKey[..8] : licenseKey);
|
||||
|
||||
// Timeout court (3 s) pour fail-fast en offline. Le HttpClient global a
|
||||
// Timeout=Infinite, donc sans ce CTS on hangerait jusqu'à ce que l'OS
|
||||
// abandonne le SYN TCP (~15-21 s sur Windows). Le call site (Refresh
|
||||
// License pendant Check Updates) catch l'exception et garde le cache
|
||||
// local, donc fail-fast est sans danger.
|
||||
using var validateCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
validateCts.CancelAfter(3_000);
|
||||
|
||||
using var httpReq = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
httpReq.Content = JsonContent.Create(req);
|
||||
using var resp = await _http.SendAsync(httpReq, ct).ConfigureAwait(false);
|
||||
using var resp = await _http.SendAsync(httpReq, validateCts.Token).ConfigureAwait(false);
|
||||
|
||||
var bodyText = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
var bodyText = await resp.Content.ReadAsStringAsync(validateCts.Token).ConfigureAwait(false);
|
||||
LicenseValidationResponse? parsed;
|
||||
try
|
||||
{
|
||||
@@ -246,20 +254,27 @@ public sealed class LicenseService : ILicenseService
|
||||
var url = TrimSlash(_serverBaseUrlProvider()) + "/download-url/" + version + "?key=" + encodedKey;
|
||||
try
|
||||
{
|
||||
// Timeout court : on est dans le hot path d'Install. Si OVH ne répond pas
|
||||
// en 5s, on fallback sur l'URL publique du manifest plutôt que de bloquer
|
||||
// la UI 100s. Le DL via peer LAN a déjà été tenté avant ce point — si on
|
||||
// est ici c'est qu'aucun peer n'avait la version.
|
||||
using var sigCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
sigCts.CancelAfter(5_000);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
using var resp = await _http.SendAsync(req, ct).ConfigureAwait(false);
|
||||
using var resp = await _http.SendAsync(req, sigCts.Token).ConfigureAwait(false);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogInformation("Signed URL endpoint returned {Status} — falling back to public URL", (int)resp.StatusCode);
|
||||
return null;
|
||||
}
|
||||
var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
var body = await resp.Content.ReadAsStringAsync(sigCts.Token).ConfigureAwait(false);
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return doc.RootElement.TryGetProperty("url", out var u) ? u.GetString() : null;
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Signed URL fetch failed, falling back to public URL");
|
||||
_logger.LogWarning("Signed URL fetch failed/timeout ({Reason}), falling back to public URL", ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user