v0.24.5 — Install Program Files, multi-PC DL reliability, polish UI
Installeur : - Bascule en Program Files (autopf) avec PrivilegesRequired=admin. L'auto-update reste fonctionnel : LauncherSelfUpdater détecte le dossier protégé et lance PS_Launcher.Updater.exe via Verb=runas (UAC à chaque MAJ). - build-installer.ps1 : détection ISCC.exe robuste (Program Files + LocalAppData pour install winget user-mode + PATH), messages d'erreur explicites avec URL et commande winget, suppression accents (compat PS 5.1 sans BOM). Téléchargements multi-PC : - DownloadManager : détection des connexions fermées prématurément (PHP-FPM request_terminate_timeout). Sans ça, segment marqué Completed=false sans exception → fichier sparse final avec trous → SHA-256 KO. Lance maintenant HttpResumableException(transient) pour que Polly retry au bon offset. - ParallelDownloadSegments : 16 → 6 par défaut pour permettre plusieurs PCs simultanés sans saturer le pool PHP-FPM OVH (~30 workers). UI : - Fenêtre maximisée par défaut au démarrage (WindowState=Maximized). - Fix maximize qui cachait le footer / barre de DL (WM_GETMINMAXINFO clamp sur work area, remplace le margin hack 7px imprécis). - Sidebar : bloc info en bas avec PS_Launcher vX.Y.Z + IPv4 locale alignés. - Copyright remonté plus près du footer (margin 28 → 8). - Settings → Health checks : boutons ▲/▼ pour réordonner, CanExecute auto. - HealthCheck refresh défaut : 5000 → 2000 ms. Bug fix UI : - MainViewModel.RebuildList preserve l'état du row actif pendant un DL. Sinon ouvrir Settings/License pendant un DL recréait les rows from scratch → UI affichait "Reprendre" + "Annuler" alors que le DL tournait. Les progress callbacks pointent maintenant sur _activeRow (résolution dynamique) au lieu de capturer le row local. Defaults config : - ServerBaseUrl : example.com → asterionvr.com (out-of-the-box). - Vive Business Streaming check : HtcConnectionUtility → rrserver. Backoffice (server/admin/licenses.php) : - Action set_max_machines : bouton "Slots" pour ajuster max_machines à chaud sur une licence existante. Refus de descendre sous le nombre de machines déjà actives. Build : - AllowUnsafeBlocks=true sur PSLauncher.App.csproj (compat WinRT generator récent qui émet du code unsafe dans WinRTGenericInstantiation.g.cs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -248,6 +251,51 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
!string.IsNullOrEmpty(ProgressDetail) ? ProgressDetail :
|
||||
StatusMessage ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Version courte du launcher au format "v0.24.0", affichée dans la sidebar.
|
||||
/// Lit l'AssemblyVersion (Major.Minor.Build) — le 4e champ (Revision) toujours .0
|
||||
/// est tronqué pour rester lisible.
|
||||
/// </summary>
|
||||
public string AppVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
var v = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
return v is null ? "v?" : $"v{v.Major}.{v.Minor}.{v.Build}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IPv4 locale du PC affichée dans la sidebar pour le diagnostic terrain
|
||||
/// (le support peut demander "quelle IP a ce poste ?" sans naviguer dans
|
||||
/// les paramètres Windows). On filtre sur l'interface qui a une default
|
||||
/// gateway IPv4 — élimine loopback, Hyper-V virtual switch, VPN inactifs,
|
||||
/// et autres interfaces sans route. "—" si aucune connectivité réseau.
|
||||
/// </summary>
|
||||
public string LocalIpAddress
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var ip = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(n => n.OperationalStatus == OperationalStatus.Up
|
||||
&& n.NetworkInterfaceType != NetworkInterfaceType.Loopback
|
||||
&& n.GetIPProperties().GatewayAddresses
|
||||
.Any(g => g.Address.AddressFamily == AddressFamily.InterNetwork))
|
||||
.SelectMany(n => n.GetIPProperties().UnicastAddresses)
|
||||
.Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.Select(a => a.Address.ToString())
|
||||
.FirstOrDefault();
|
||||
return ip ?? "—";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "—";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MainViewModel(
|
||||
IInstallationRegistry registry,
|
||||
IProcessLauncher processLauncher,
|
||||
@@ -383,6 +431,15 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
/// </summary>
|
||||
private void RebuildList()
|
||||
{
|
||||
// Si un DL/verify est en cours, on capture la row active pour transférer
|
||||
// son état (Downloading/Verifying + progression) sur la nouvelle instance.
|
||||
// Sans ça, l'UI repasse sur le nouveau row qui est en AvailableIdle avec
|
||||
// ResumableBytes lu depuis state.json → boutons "Reprendre" + "Annuler"
|
||||
// affichés alors que le DL continue derrière (footer qui avance).
|
||||
var oldActive = _activeRow;
|
||||
var preserveActive = oldActive is not null
|
||||
&& oldActive.State is VersionRowState.Downloading or VersionRowState.Verifying;
|
||||
|
||||
var installed = _registry.Scan().ToDictionary(v => v.Version);
|
||||
var remote = _lastManifest?.Versions ?? new List<VersionManifest>();
|
||||
var remoteByVer = remote.ToDictionary(v => v.Version);
|
||||
@@ -419,6 +476,26 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
&& _licenseService.CanDownloadVersion(_license, r.Remote);
|
||||
}
|
||||
|
||||
// Si un DL était en vol, propage son état sur la nouvelle instance avant
|
||||
// que les bindings UI ne s'attachent : State Downloading/Verifying +
|
||||
// progression copiés, ResumableBytes forcé à 0 pour cacher les boutons
|
||||
// "Reprendre" / "Annuler" tant que le DL tourne. _activeRow est repointé
|
||||
// sur la nouvelle instance pour que les progress callbacks (résolus à
|
||||
// l'exécution via _activeRow, pas via une capture closure) ciblent le
|
||||
// bon objet et que la barre de progress de la row avance live.
|
||||
if (preserveActive && oldActive is not null)
|
||||
{
|
||||
var matching = rows.FirstOrDefault(r => r.Version == oldActive.Version);
|
||||
if (matching is not null)
|
||||
{
|
||||
matching.State = oldActive.State;
|
||||
matching.ProgressPercent = oldActive.ProgressPercent;
|
||||
matching.ProgressDetail = oldActive.ProgressDetail;
|
||||
matching.ResumableBytes = 0;
|
||||
_activeRow = matching;
|
||||
}
|
||||
}
|
||||
|
||||
// Featured : plus haute installée, sinon plus haute distante
|
||||
var featured = rows.FirstOrDefault(r => r.IsInstalled) ?? rows.FirstOrDefault();
|
||||
FeaturedVersion = featured;
|
||||
@@ -809,20 +886,29 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
ProgressPercent = 0;
|
||||
row.ProgressDetail = null;
|
||||
row.ProgressPercent = 0;
|
||||
var dlProgress = new Progress<DownloadProgress>(p => UpdateDlProgress(row, p));
|
||||
// Les progress callbacks ciblent _activeRow (résolution dynamique) plutôt que
|
||||
// de capturer le local `row` par closure — sinon, si un RebuildList intervient
|
||||
// en cours de DL (cf. OpenSettings/OpenLicense), l'instance row capturée serait
|
||||
// orpheline et la barre per-row n'avancerait plus côté UI.
|
||||
var dlProgress = new Progress<DownloadProgress>(p =>
|
||||
{
|
||||
if (_activeRow is { } target) UpdateDlProgress(target, p);
|
||||
});
|
||||
// Reporter dédié pour la phase verify SHA-256 (peut prendre 30s-3min sur 14 Go).
|
||||
// On bascule row.State sur Verifying à la première remontée, ce qui change
|
||||
// le badge de la row de "⬇ Téléchargement…" vers "🔍 Vérification…" — les
|
||||
// deux phases sont distinctes côté UX bien que `DownloadAsync` les enchaîne.
|
||||
// On bascule la row active sur Verifying à la première remontée, ce qui change
|
||||
// le badge de "⬇ Téléchargement…" vers "🔍 Vérification…" — les deux phases
|
||||
// sont distinctes côté UX bien que `DownloadAsync` les enchaîne.
|
||||
var hashProgress = new Progress<double>(pct =>
|
||||
{
|
||||
if (row.State == VersionRowState.Downloading)
|
||||
row.State = VersionRowState.Verifying;
|
||||
var target = _activeRow;
|
||||
if (target is null) return;
|
||||
if (target.State == VersionRowState.Downloading)
|
||||
target.State = VersionRowState.Verifying;
|
||||
var percent = (int)Math.Round(pct * 100.0);
|
||||
ProgressPercent = percent;
|
||||
row.ProgressPercent = percent;
|
||||
ProgressDetail = Strings.ProgressVerifying(row.Version, percent);
|
||||
row.ProgressDetail = ProgressDetail;
|
||||
target.ProgressPercent = percent;
|
||||
ProgressDetail = Strings.ProgressVerifying(target.Version, percent);
|
||||
target.ProgressDetail = ProgressDetail;
|
||||
});
|
||||
// NB: pas de StatusMessage = "Téléchargement…" ici — on garde « Préparation… »
|
||||
// visible pendant la phase HEAD/SetLength, puis le footer passe directement
|
||||
|
||||
Reference in New Issue
Block a user