From 07ead6011f285c59faabd35fcc52c827122b95d6 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sun, 3 May 2026 20:08:32 +0200 Subject: [PATCH] =?UTF-8?q?v0.23.2=20=E2=80=94=20Fix=20AccessViolation=20p?= =?UTF-8?q?endant=20le=20d=C3=A9marrage=20de=20SteamVR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cause : race condition. Quand l'utilisateur lance SteamVR alors que le launcher tourne déjà, vrserver.exe apparaît dans la liste des process avant que ses drivers et ses shared memories soient prêts à servir des requêtes vtable. La séquence problématique : 1. tick health loop → IsVrServerRunning() = true (vrserver vient juste d'apparaître dans la process list) 2. VR_InitInternal2 → succeed (init est tolérant, ne lit pas la SHM) 3. VR_GetGenericInterface → vtable pointer valide 4. premier appel vtable (IsTrackedDeviceConnected, slot 21) → lit dans une shared memory pas encore allouée par vrserver → AccessViolation 5. SEH passe au travers du CLR via Marshal.GetDelegateForFunctionPointer → process killed sans que catch (Exception) ne se déclenche. Stack confirmée par l'Event Viewer Windows : System.AccessViolationException at OpenVrService.Query(System.String) at SystemHealthService.CheckVrDevice(...) at health loop background task Fix : cooldown de 8 s après la première détection de vrserver. On note le timestamp UTC de la première apparition, et on refuse l'init tant que moins de 8 s n'ont passé. Pendant le warmup, la pill reste sur "SteamVR non lancé" (HmdAbsent), puis bascule sur Ready au tick suivant. Si vrserver disparaît, on reset le timestamp + on tear-down la session proprement, prêts pour un prochain cycle de démarrage. 8 s = compromis : sur SSD le ramp-up des drivers SteamVR prend ~3-5 s, sur HDD ça peut monter à 7 s. La marge couvre les deux. Bonus diag : flush Serilog avant que le process meure dans le hook AppDomain.UnhandledException. Sans ça, la trace de la fatale n'arrivait jamais sur disque (exactement ce qu'on a vu dans les logs : silence total avant le redémarrage suivant). Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- src/PSLauncher.App/App.xaml.cs | 14 ++++- src/PSLauncher.App/PSLauncher.App.csproj | 6 +- .../Health/OpenVR/OpenVrService.cs | 58 +++++++++++++++++-- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 741630d..8e47839 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "0.23.1" +#define MyAppVersion "0.23.2" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/App.xaml.cs b/src/PSLauncher.App/App.xaml.cs index 5924e2e..cef361f 100644 --- a/src/PSLauncher.App/App.xaml.cs +++ b/src/PSLauncher.App/App.xaml.cs @@ -97,7 +97,19 @@ public partial class App : Application Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory); AppDomain.CurrentDomain.UnhandledException += (_, args) => - Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain)"); + { + // CRITIQUE : on flush AVANT que le process meure. Sans ça, sur les + // exceptions de corruption d'état (AccessViolation depuis P/Invoke, + // SEH natif…) le buffer Serilog n'est jamais écrit sur disque, et + // le crash est totalement invisible dans les logs — exactement le + // mode de panne qu'on a eu avec OpenVR pendant le démarrage SteamVR. + try + { + Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain) IsTerminating={IsTerminating}", args.IsTerminating); + Log.CloseAndFlush(); + } + catch { /* dernier recours — on ne peut plus rien faire */ } + }; DispatcherUnhandledException += (_, args) => { Log.Error(args.Exception, "Unhandled UI exception"); diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index f9cf722..2bcbffe 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -15,9 +15,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.23.1 - 0.23.1.0 - 0.23.1.0 + 0.23.2 + 0.23.2.0 + 0.23.2.0 true diff --git a/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs b/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs index 0eea724..9b311b5 100644 --- a/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs +++ b/src/PSLauncher.Core/Health/OpenVR/OpenVrService.cs @@ -28,6 +28,18 @@ public sealed class OpenVrService : IOpenVrService, IDisposable private bool _resolverEnsured; private bool _initAttempted; private VrRuntimeState _lastLoggedState = (VrRuntimeState)(-99); // sentinelle « jamais loggé » + + /// + /// Timestamp UTC de la PREMIÈRE détection de vrserver.exe lancé après une + /// période où il était absent. Remis à null si vrserver disparaît. Sert à + /// imposer un cooldown avant d'oser init OpenVR : entre l'apparition de + /// vrserver dans la liste process et le moment où ses drivers + ses shared + /// memories sont prêts à servir des requêtes vtable, il y a typiquement 5-10 s + /// de race window pendant laquelle + /// + premier appel = AccessViolation (lecture mémoire pas encore mappée). + /// + private DateTime? _vrServerFirstSeenUtc; + private const double VrServerSettleSeconds = 8.0; private uint _initToken; private IntPtr _vrSystemVTable = IntPtr.Zero; @@ -146,17 +158,28 @@ public sealed class OpenVrService : IOpenVrService, IDisposable return VrRuntimeState.HmdAbsent; } - // Garde supplémentaire avant l'init : vrserver.exe DOIT être actif. Sans - // lui, VR_InitInternal2 en mode Background peut soit hang, soit charger - // des drivers (lighthouse, oculus_link…) qui plantent en SEH si SteamVR - // n'a jamais été démarré sur cette session. Cette garde évite ce scénario - // au prix de ne plus rien lire jusqu'à ce que l'utilisateur lance SteamVR - // — ce qui est le comportement attendu de toute façon. + // Garde 1 : vrserver.exe DOIT être actif. Sans lui, VR_InitInternal2 en + // mode Background peut soit hang, soit charger des drivers (lighthouse, + // oculus_link…) qui plantent en SEH. if (!IsVrServerRunning()) { return VrRuntimeState.HmdAbsent; } + // Garde 2 : on attend que vrserver soit en l'air depuis assez longtemps + // pour que ses drivers et ses shared memories soient initialisés. + // Sinon : init succeed → premier vtable call → AccessViolation natif + // (la shared memory n'est pas encore mappée). Le SEH passe au travers du + // CLR via Marshal.GetDelegateForFunctionPointer et tue le process. + // 8s = compromis entre réactivité (l'utilisateur a lancé SteamVR, on veut + // pas attendre 30 s) et fiabilité (sur SSD c'est ~3-5 s, sur HDD ça peut + // monter à 7 s). On garde HmdAbsent affiché pendant le warmup ; quand le + // ticker se redéclenche après le délai, on enchaînera proprement sur Ready. + if (!HasVrServerSettled()) + { + return VrRuntimeState.HmdAbsent; + } + // Init en mode Background : on lit les states sans devenir une scene app. // L'init peut prendre 100-500 ms la première fois ; bloquer le caller est OK // car le health loop tourne sur thread dédié. @@ -371,9 +394,32 @@ public sealed class OpenVrService : IOpenVrService, IDisposable _vrServerCacheValue = false; } _vrServerCacheUntil = now.AddSeconds(2); + + // Update first-seen tracking : on note l'instant où vrserver apparaît, + // et on l'oublie quand il disparaît. C'est ce timestamp qu'on consulte + // dans EnsureReady pour décider si la fenêtre de race est passée. + if (_vrServerCacheValue && _vrServerFirstSeenUtc is null) + { + _vrServerFirstSeenUtc = now; + _logger.LogInformation("vrserver.exe just appeared — waiting {Secs}s for SteamVR drivers/IPC to settle before init", VrServerSettleSeconds); + } + else if (!_vrServerCacheValue && _vrServerFirstSeenUtc is not null) + { + _logger.LogInformation("vrserver.exe disappeared — resetting OpenVR session"); + _vrServerFirstSeenUtc = null; + // Si on était Ready, l'invalider : la prochaine session SteamVR exigera un nouvel init + if (_initToken != 0) ShutdownInternal(); + _initAttempted = false; + } return _vrServerCacheValue; } + private bool HasVrServerSettled() + { + if (_vrServerFirstSeenUtc is null) return false; + return (DateTime.UtcNow - _vrServerFirstSeenUtc.Value).TotalSeconds >= VrServerSettleSeconds; + } + private void MarkSessionBroken() { ShutdownInternal();