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:
@@ -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");
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.23.1</Version>
|
||||
<AssemblyVersion>0.23.1.0</AssemblyVersion>
|
||||
<FileVersion>0.23.1.0</FileVersion>
|
||||
<Version>0.23.2</Version>
|
||||
<AssemblyVersion>0.23.2.0</AssemblyVersion>
|
||||
<FileVersion>0.23.2.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -28,6 +28,18 @@ public sealed class OpenVrService : IOpenVrService, IDisposable
|
||||
private bool _resolverEnsured;
|
||||
private bool _initAttempted;
|
||||
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 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();
|
||||
|
||||
Reference in New Issue
Block a user