v0.23.2 — Fix AccessViolation pendant le démarrage de SteamVR
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) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "0.23.1"
|
#define MyAppVersion "0.23.2"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -97,7 +97,19 @@ public partial class App : Application
|
|||||||
Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory);
|
Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory);
|
||||||
|
|
||||||
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
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) =>
|
DispatcherUnhandledException += (_, args) =>
|
||||||
{
|
{
|
||||||
Log.Error(args.Exception, "Unhandled UI exception");
|
Log.Error(args.Exception, "Unhandled UI exception");
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>0.23.1</Version>
|
<Version>0.23.2</Version>
|
||||||
<AssemblyVersion>0.23.1.0</AssemblyVersion>
|
<AssemblyVersion>0.23.2.0</AssemblyVersion>
|
||||||
<FileVersion>0.23.1.0</FileVersion>
|
<FileVersion>0.23.2.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ public sealed class OpenVrService : IOpenVrService, IDisposable
|
|||||||
private bool _resolverEnsured;
|
private bool _resolverEnsured;
|
||||||
private bool _initAttempted;
|
private bool _initAttempted;
|
||||||
private VrRuntimeState _lastLoggedState = (VrRuntimeState)(-99); // sentinelle « jamais loggé »
|
private VrRuntimeState _lastLoggedState = (VrRuntimeState)(-99); // sentinelle « jamais loggé »
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="OpenVRNative.GetVTableSlot"/>
|
||||||
|
/// + premier appel = AccessViolation (lecture mémoire pas encore mappée).
|
||||||
|
/// </summary>
|
||||||
|
private DateTime? _vrServerFirstSeenUtc;
|
||||||
|
private const double VrServerSettleSeconds = 8.0;
|
||||||
private uint _initToken;
|
private uint _initToken;
|
||||||
private IntPtr _vrSystemVTable = IntPtr.Zero;
|
private IntPtr _vrSystemVTable = IntPtr.Zero;
|
||||||
|
|
||||||
@@ -146,17 +158,28 @@ public sealed class OpenVrService : IOpenVrService, IDisposable
|
|||||||
return VrRuntimeState.HmdAbsent;
|
return VrRuntimeState.HmdAbsent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Garde supplémentaire avant l'init : vrserver.exe DOIT être actif. Sans
|
// Garde 1 : vrserver.exe DOIT être actif. Sans lui, VR_InitInternal2 en
|
||||||
// lui, VR_InitInternal2 en mode Background peut soit hang, soit charger
|
// mode Background peut soit hang, soit charger des drivers (lighthouse,
|
||||||
// des drivers (lighthouse, oculus_link…) qui plantent en SEH si SteamVR
|
// oculus_link…) qui plantent en SEH.
|
||||||
// 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.
|
|
||||||
if (!IsVrServerRunning())
|
if (!IsVrServerRunning())
|
||||||
{
|
{
|
||||||
return VrRuntimeState.HmdAbsent;
|
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.
|
// 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
|
// 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é.
|
// car le health loop tourne sur thread dédié.
|
||||||
@@ -371,9 +394,32 @@ public sealed class OpenVrService : IOpenVrService, IDisposable
|
|||||||
_vrServerCacheValue = false;
|
_vrServerCacheValue = false;
|
||||||
}
|
}
|
||||||
_vrServerCacheUntil = now.AddSeconds(2);
|
_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;
|
return _vrServerCacheValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool HasVrServerSettled()
|
||||||
|
{
|
||||||
|
if (_vrServerFirstSeenUtc is null) return false;
|
||||||
|
return (DateTime.UtcNow - _vrServerFirstSeenUtc.Value).TotalSeconds >= VrServerSettleSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
private void MarkSessionBroken()
|
private void MarkSessionBroken()
|
||||||
{
|
{
|
||||||
ShutdownInternal();
|
ShutdownInternal();
|
||||||
|
|||||||
Reference in New Issue
Block a user