From 43a6070a65c878229e729428cc24bf099f8e4ceb Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 22 Jun 2026 14:13:18 +0200 Subject: [PATCH] =?UTF-8?q?v1.0.1=20=E2=80=94=20Fix=20SteamVR=20merge=20:?= =?UTF-8?q?=20scope=20aux=20blocs=20system.generated.*=20uniquement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug : à chaque install, la popup « Vérification de la santé système / X blocs vont être mis à jour » apparaissait alors que la source `_steamvr/ steamvr.vrsettings` n'avait pas changé entre les sous-versions. L'opérateur voyait le launcher toucher au fichier SteamVR de manière répétitive et inutile. Root cause : le diff comparait TOUS les blocs racine du source vs target. Or le fichier source contient (légitimement, pour la doc opérateur) des blocs user-/machine-spécifiques copiés depuis une machine de référence : • DesktopUI (position fenêtres SteamVR — varie par poste) • GpuSpeed (calibration GPU — RTX 3080 chez l'opérateur vs autre GPU chez le client) • LastKnown (HMD info — Focus3 ou autre) • dashboard, steamvr (settings UI — installID utilisateur, etc.) • trackers (mapping device — déjà configuré côté client) Ces blocs DIFFÈRENT toujours entre la machine de référence (où le source a été capturé) et chaque poste client → faux positif de diff systématique. Fix : on restreint la diff + le push aux blocs racine `system.generated.*` (typiquement system.generated.openxr.proserve_ue_5_5.proserve_ue_5_5.exe, etc.) qui contiennent les bindings tracker workshop URLs — la VRAIE config que le launcher est censé pousser. Tout le reste du fichier source est maintenant ignoré. Sémantique précise pour un bloc system.generated.* : • ABSENT côté target → push complet (les 4 leaf keys CurrentURL, PreviousURL, AutosaveURL, NeedToUpdateAutosave) • PRÉSENT côté target → check sur les seules leaf keys *_CurrentURL_openxr et *_PreviousURL_openxr. Si elles matchent → skip silencieux. Si elles diffèrent (= opérateur a updaté la binding workshop) → réécriture des 2 leafs ciblées, les autres (AutosaveURL, NeedToUpdate) sont laissées intactes (gérées par SteamVR). Cleanup : helpers `DeepMergeInto` et `WouldDeepMergeChange` retirés (plus référencés). Nouveau helpers ciblés `OpenXrBindingUrlsDiffer` (check) et `ReplaceOpenXrBindingUrls` (write). Doc XML mise à jour côté interface. Bump : 1.0.0 → 1.0.1 (patch fix). Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- src/PSLauncher.App/PSLauncher.App.csproj | 6 +- .../SteamVr/ISteamVrSettingsDeployer.cs | 30 ++-- .../SteamVr/SteamVrSettingsDeployer.cs | 167 +++++++++++------- 4 files changed, 122 insertions(+), 83 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index cac7143..ee3b21e 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "1.0.0" +#define MyAppVersion "1.0.1" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 5a00034..50359f3 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -18,9 +18,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 1.0.0 - 1.0.0.0 - 1.0.0.0 + 1.0.1 + 1.0.1.0 + 1.0.1.0 true diff --git a/src/PSLauncher.Core/SteamVr/ISteamVrSettingsDeployer.cs b/src/PSLauncher.Core/SteamVr/ISteamVrSettingsDeployer.cs index 78447b8..fa935dc 100644 --- a/src/PSLauncher.Core/SteamVr/ISteamVrSettingsDeployer.cs +++ b/src/PSLauncher.Core/SteamVr/ISteamVrSettingsDeployer.cs @@ -1,20 +1,26 @@ namespace PSLauncher.Core.SteamVr; /// -/// Fusionne récursivement le steamvr.vrsettings bundled dans le ZIP -/// PROSERVE (sous-dossier _steamvr/) dans le fichier SteamVR de l'utilisateur. +/// Fusionne le steamvr.vrsettings bundled dans le ZIP PROSERVE +/// (sous-dossier _steamvr/) dans le fichier SteamVR de l'utilisateur. /// -/// Sémantique de merge (deep, récursive) : +/// Sémantique de merge — SCOPED aux blocs racine system.generated.* : /// -/// Clé absente côté cible → ajoutée (clone profond du source) -/// Clé présente des deux côtés, valeurs JSON objet → recursivement fusionnées -/// (les sous-clés cible non touchées par le source sont préservées — -/// critique pour des blocs comme "trackers" qui contiennent -/// potentiellement déjà d'autres devices) -/// Clé présente, valeurs primitives ou de types différents (string, -/// bool, number, array) → la valeur source écrase celle de cible -/// Clés racine côté cible qui ne sont PAS dans le source → 100 % -/// préservées (on ne touche QUE ce que le source apporte) +/// SEULS les blocs racine system.generated.* sont considérés. +/// Tous les autres blocs (DesktopUI, GpuSpeed, LastKnown, +/// dashboard, steamvr, trackers, …) sont user-/machine- +/// spécifiques et VARIENT légitimement entre postes — le launcher ne +/// les touche jamais (sinon faux positifs de diff à chaque install). +/// Bloc system.generated.* ABSENT côté cible → push complet du +/// bloc tel quel (les 4 leaf keys fournies par l'installer dans le ZIP). +/// Bloc PRÉSENT côté cible → réécriture des leaf keys +/// *_CurrentURL_openxr et *_PreviousURL_openxr uniquement. +/// Les autres leaf keys (_AutosaveURL_openxr, +/// _NeedToUpdateAutosave_openxr) restent intactes — gérées par +/// SteamVR. +/// La décision "merge nécessaire" repose UNIQUEMENT sur la diff des +/// CurrentURL/PreviousURL (ou l'absence du bloc). Pas de popup si l'install +/// est ré-effectué et que ces valeurs n'ont pas bougé. /// /// /// Tue préalablement SteamVR + Vive Business Streaming pour éviter qu'ils ne diff --git a/src/PSLauncher.Core/SteamVr/SteamVrSettingsDeployer.cs b/src/PSLauncher.Core/SteamVr/SteamVrSettingsDeployer.cs index 99f9854..93c2079 100644 --- a/src/PSLauncher.Core/SteamVr/SteamVrSettingsDeployer.cs +++ b/src/PSLauncher.Core/SteamVr/SteamVrSettingsDeployer.cs @@ -1,5 +1,6 @@ using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Win32; using PSLauncher.Models; @@ -17,6 +18,32 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer /// Nom canonique du fichier de settings SteamVR (côté source ET cible). private const string SettingsFileName = "steamvr.vrsettings"; + /// + /// Pattern d'identification d'un bloc auto-généré par SteamVR pour une + /// application OpenXR donnée. Ex : + /// system.generated.openxr.proserve_ue_5_5.proserve_ue_5_5.exe. + /// + /// Le launcher ne considère QUE ces blocs pour la diff + le push. Tous les + /// autres blocs racine du source (DesktopUI, GpuSpeed, + /// LastKnown, dashboard, steamvr, trackers, …) + /// sont user-/machine-spécifiques et VARIENT légitimement entre postes — + /// les checker provoquerait des faux positifs de diff à chaque install. + /// + private static readonly Regex SystemGeneratedKey = + new(@"^system\.generated\.", RegexOptions.Compiled); + + /// + /// Leaf keys dans un bloc system.generated.* qui pilotent la diff. + /// Si l'une ou l'autre diffère entre source et target → le bloc est marqué + /// "à mettre à jour". Les autres leaf keys (_AutosaveURL_openxr, + /// _NeedToUpdateAutosave_openxr) ne sont PAS considérées pour la + /// décision de diff, mais elles sont quand même écrites lors du push initial + /// d'un bloc absent du target. + /// + private static bool IsRelevantOpenXrLeaf(string leafKey) => + leafKey.EndsWith("_CurrentURL_openxr", StringComparison.Ordinal) + || leafKey.EndsWith("_PreviousURL_openxr", StringComparison.Ordinal); + /// /// Délai d'attente entre le kill des processus et l'ouverture du fichier. /// SteamVR garde un handle sur steamvr.vrsettings et le flush ses changements @@ -111,27 +138,36 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer $"Cible JSON invalide : {ex.Message}"); } - // 7) Deep-compare. On compte les clés RACINE qui changeraient (added ou - // modified) — c'est le compteur lisible pour l'opérateur. Les sous-clés - // d'un bloc qui change ne sont pas comptées individuellement. + // 7) Diff scoped : SEULS les blocs racine `system.generated.*` sont + // considérés. Les autres (DesktopUI, GpuSpeed, LastKnown, dashboard, + // steamvr, trackers, …) sont user-/machine-spécifiques et varient + // légitimement entre postes — les checker générerait des faux positifs + // de diff à chaque install. Cf. SystemGeneratedKey. + // + // Pour chaque bloc system.generated.* : + // • ABSENT côté target → on push le bloc entier au merge (= compte + // comme un "change") + // • PRÉSENT côté target → on compare uniquement les leaf keys + // `*_CurrentURL_openxr` et `*_PreviousURL_openxr`. Si l'une diffère, + // c'est un "change" (l'opérateur a édité ses bindings). Sinon, skip. int rootKeysChanging = 0; foreach (var (key, srcValue) in sourceObj) { + // Whitelist : on n'examine QUE les blocs system.generated.* + if (!SystemGeneratedKey.IsMatch(key)) continue; + if (!targetObj.ContainsKey(key)) { rootKeysChanging++; continue; } var tgtValue = targetObj[key]; + if (srcValue is JsonObject srcChild && tgtValue is JsonObject tgtChild) { - if (WouldDeepMergeChange(tgtChild, srcChild)) + if (OpenXrBindingUrlsDiffer(tgtChild, srcChild)) rootKeysChanging++; } - else if (!JsonNodesEqual(tgtValue, srcValue)) - { - rootKeysChanging++; - } } var outcome = rootKeysChanging > 0 @@ -281,28 +317,38 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer // Les compteurs top-level reflètent uniquement les clés RACINE // (remplacées ou ajoutées) parce que c'est le niveau visible dans // les logs et le seul niveau que l'opérateur a besoin de monitorer. + // Merge scoped : on n'agit QUE sur les blocs racine system.generated.* + // (cf. justification dans CheckMergeNeededAsync). + // • ABSENT côté target → push complet du bloc tel quel + // (= ce que l'installer fournit, avec ses 4 leaf keys) + // • PRÉSENT côté target → écrase les leaf keys CurrentURL + + // PreviousURL avec les valeurs du source. Les autres leaf keys + // (AutosaveURL, NeedToUpdate) restent comme côté target — gérées + // par SteamVR, pas touchées par le launcher. int replaced = 0, added = 0; foreach (var (key, srcValue) in sourceObj.ToList()) { + if (!SystemGeneratedKey.IsMatch(key)) continue; + if (!targetObj.ContainsKey(key)) { + // Bloc absent côté target → push complet du source + // (les 4 leaf keys, telles que fournies par l'installer). targetObj[key] = srcValue?.DeepClone(); added++; continue; } - // Clé présente des deux côtés. Si les deux valeurs sont des - // objets JSON, on deep-merge ; sinon le source remplace. if (srcValue is JsonObject srcChild && targetObj[key] is JsonObject tgtChild) { - DeepMergeInto(tgtChild, srcChild); + // Bloc présent → on n'écrit QUE les leaf keys CurrentURL + + // PreviousURL depuis source (replace). Si elles matchent déjà, + // c'est un no-op au final (mais on tag quand même replaced + // car le check a déclenché un merge). + int leafTouched = ReplaceOpenXrBindingUrls(tgtChild, srcChild); + if (leafTouched > 0) replaced++; } - else - { - targetObj[key] = srcValue?.DeepClone(); - } - replaced++; } // 8. Write atomique via .tmp + rename. SteamVR utilise un JSON @@ -321,6 +367,7 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer _logger.LogInformation( "SteamVR settings merge OK: replaced={Replaced}, added={Added}, target={Path}", replaced, added, targetPath); + return new SteamVrMergeResult( SteamVrMergeStatus.Merged, replaced, added, targetPath, killed); } @@ -431,43 +478,6 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer /// L'ordre des entrées dans la liste compte : Vive Business Streaming /// doit être en TÊTE car il relance SteamVR sinon. /// - /// - /// Fusionne récursivement dans . - /// Règles par clé : - /// • absente côté cible → ajout (clone profond de la valeur source) - /// • présente des deux côtés ET les deux valeurs sont des objets JSON → recurse - /// • sinon → la valeur source écrase celle de cible - /// - /// Les arrays JSON sont traités comme des "primitives" (= replace complet, - /// pas de merge par index ni dédup). SteamVR n'utilise pas d'arrays dans - /// ses settings de toute façon, c'est juste pour avoir un comportement - /// prévisible si jamais. - /// - /// - /// Dry-run récursif : applique mentalement le même algo que - /// et retourne true dès qu'au moins UNE clé du - /// source serait ajoutée ou modifiée côté target. Short-circuit pour ne - /// pas parcourir le JSON entier inutilement quand on a déjà trouvé un diff. - /// - private static bool WouldDeepMergeChange(JsonObject target, JsonObject source) - { - foreach (var (key, srcValue) in source) - { - if (!target.ContainsKey(key)) return true; - var tgtValue = target[key]; - - if (srcValue is JsonObject srcChild && tgtValue is JsonObject tgtChild) - { - if (WouldDeepMergeChange(tgtChild, srcChild)) return true; - } - else if (!JsonNodesEqual(tgtValue, srcValue)) - { - return true; - } - } - return false; - } - /// /// Comparaison d'égalité structurelle entre deux JsonNode (objects, arrays, /// primitives). System.Text.Json n'expose pas DeepEquals avant .NET 9 ; @@ -513,26 +523,49 @@ public sealed class SteamVrSettingsDeployer : ISteamVrSettingsDeployer return false; } - private static void DeepMergeInto(JsonObject target, JsonObject source) + /// + /// Dry-run pour un bloc system.generated.* déjà présent côté target. + /// Retourne true si une des leaf keys pertinentes (_CurrentURL_openxr + /// ou _PreviousURL_openxr) DIFFÈRE entre source et target. Les autres + /// leaf keys (AutosaveURL, NeedToUpdate, …) sont ignorées pour la décision. + /// + private static bool OpenXrBindingUrlsDiffer(JsonObject target, JsonObject source) { - foreach (var (key, srcValue) in source.ToList()) + foreach (var (leafKey, srcValue) in source) { - if (!target.ContainsKey(key)) + if (!IsRelevantOpenXrLeaf(leafKey)) continue; + if (!target.TryGetPropertyValue(leafKey, out var tgtValue)) return true; // absent du target + if (!JsonNodesEqual(tgtValue, srcValue)) return true; // valeur différente + } + return false; + } + + /// + /// Merge effectif sur un bloc system.generated.* déjà présent côté + /// target. Réécrit les leaf keys CurrentURL et PreviousURL depuis le source, + /// laisse les autres leaf keys (_AutosaveURL_openxr, + /// _NeedToUpdateAutosave_openxr) intactes — c'est SteamVR qui les + /// gère, le launcher ne s'en mêle pas. + /// Retourne le nombre de leaf keys effectivement touchées (= 0 si source et + /// target ont déjà les mêmes URLs, sinon > 0). + /// + private static int ReplaceOpenXrBindingUrls(JsonObject target, JsonObject source) + { + int touched = 0; + foreach (var (leafKey, leafValue) in source.ToList()) + { + if (!IsRelevantOpenXrLeaf(leafKey)) continue; + // Skip si target a déjà la même valeur exacte (no-op silencieux, + // pas la peine de marquer touched). + if (target.TryGetPropertyValue(leafKey, out var existing) + && JsonNodesEqual(existing, leafValue)) { - target[key] = srcValue?.DeepClone(); continue; } - - if (srcValue is JsonObject srcChild - && target[key] is JsonObject tgtChild) - { - DeepMergeInto(tgtChild, srcChild); - } - else - { - target[key] = srcValue?.DeepClone(); - } + target[leafKey] = leafValue?.DeepClone(); + touched++; } + return touched; } private async Task KillProcessesAsync(