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>
63 lines
2.3 KiB
C#
63 lines
2.3 KiB
C#
namespace PSLauncher.Core.Lan;
|
||
|
||
/// <summary>
|
||
/// URL d'un peer LAN qui a déjà la version demandée et a passé la vérification
|
||
/// (taille + SHA-256 du manifest match la réponse <c>/v1/has/</c>). À utiliser
|
||
/// dans le DownloadJob comme URL initiale au lieu de l'URL OVH.
|
||
/// </summary>
|
||
public sealed record PeerSource(Uri ZipUrl, string PeerHost);
|
||
|
||
/// <summary>
|
||
/// Helper interne aux probers (PeerSourceResolver, PeerManifestFetcher) pour
|
||
/// fusionner peers actifs (beacons UDP récents), peers connus (cache disque),
|
||
/// peers manuels (config). Évite la duplication entre les 2 fetchers.
|
||
/// </summary>
|
||
internal static class PeerCandidates
|
||
{
|
||
public static List<string> Build(ILanDiscoveryService discovery, IEnumerable<string> manualUrls)
|
||
{
|
||
var candidates = new List<string>();
|
||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||
|
||
// 1) Peers actifs (UDP beacon < 60s) — les plus fiables
|
||
foreach (var p in discovery.ListDiscovered())
|
||
{
|
||
var url = $"http://{p.Host}:{p.Port}";
|
||
if (seen.Add(url)) candidates.Add(url);
|
||
}
|
||
// 2) Peers connus (cache disque, runs précédents) — essayés rapidement même
|
||
// si pas encore reçu de beacon ce run (cold start, < 15s d'uptime)
|
||
foreach (var p in discovery.ListKnown())
|
||
{
|
||
var url = $"http://{p.Host}:{p.Port}";
|
||
if (seen.Add(url)) candidates.Add(url);
|
||
}
|
||
// 3) Peers manuels (config user) — fallback / override explicite
|
||
foreach (var url in manualUrls)
|
||
{
|
||
var trimmed = url.Trim().TrimEnd('/');
|
||
if (string.IsNullOrEmpty(trimmed)) continue;
|
||
if (seen.Add(trimmed)) candidates.Add(trimmed);
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Trouve un peer LAN qui a la version demandée. Combine peers découverts auto
|
||
/// (UDP discovery) et peers manuels (config), probe chacun avec un timeout court,
|
||
/// retourne le premier qui matche le SHA-256 attendu. Null si aucun → fallback OVH.
|
||
///
|
||
/// Budget total : <c>ProbeTimeoutMs</c> × max 5 peers = ~5 s pire cas. Ne ralentit
|
||
/// jamais le path OVH au-delà.
|
||
/// </summary>
|
||
public interface IPeerSourceResolver
|
||
{
|
||
Task<PeerSource?> TryResolveAsync(
|
||
string version,
|
||
long expectedSize,
|
||
string expectedSha256,
|
||
CancellationToken ct);
|
||
}
|