Cycle de vie de la session

Documentation de la version v0.1.0-beta.3Voir la source sur GitHub

Traduction de la page originale en anglais d’OmsiLaunch 0.1.0-beta3. La page anglaise fait foi : en cas de divergence, la page anglaise et le code prévalent.

Cette page décrit comment une session OmsiLaunch parcourt SessionState de Created jusqu'à Completed ou Failed : quel composant définit chaque état, quels événements de télémétrie du plugin déclenchent les transitions, comment fonctionne le timeout de démarrage, ce que signifie l'arrêt (arrêt forcé), ce que renvoie WaitForAsync, quels états sont terminaux, quels états ne sont jamais ou à peine observables, et quelles garanties offre le propriétaire CLI. Tout ce qui suit est tiré de OmsiLaunchService.StartAsync, SuperviseAsync et ApplyTelemetry (src/OmsiLaunch.Core/OmsiLaunchService.cs), de PluginRuntime (src/OmsiLaunch.Plugin/PluginRuntime.cs) et de OwnerSession (tools/OmsiLaunch.Cli/Program.cs).

Pages associées : API publique, codes d'erreur, transactions et récupération, plugin permanent, contrôle runtime, plan de contrôle local, zone de notification Windows, référence CLI, état de la validation à l'exécution, le répertoire .omsilaunch.

Vue d'ensemble#

PlanSessionAsync                       (no state; returns a SessionPlan)
StartSessionAsync ─ caller thread ─────────────────────────────────────────────
  Created
  AcquiringInstallationLock            lease Local\OmsiLaunch.Installation.<hash>
  RecoveringPreviousTransaction        stale journal restored before anything is read
  Snapshotting → ApplyingConfiguration journal Prepared, overlays written, deletions removed, Applied
  DeployingRuntime                     journal RuntimeDeployed (plugin is permanent; nothing copied)
  CreatingStartupHandoff               handoff, telemetry slot, runtime mailbox; journal HandoffCreated
  StartingProcess                      CreateProcessW Omsi.exe
  WaitingForPlugin                     journal ProcessStarted (PID, creation time, exe path) → handle returned
SuperviseAsync ─ background task ──────────────────────────────────────────────
  PluginBootstrap                      telemetry plugin.started
  StartingWorld                        telemetry world.starting (NEW_MAP only)
  Running                              telemetry gameplay.entered
  ProcessExited                        OMSI exited or was terminated; journal ProcessExited
  Restoring                            exact restore of every session-owned file
  CleaningRuntime                      restore verified; journal removed; backups removed
  Completed                            stores disposed, lease released
  Failed                               from any point above; restore still runs

Référence de SessionState#

Valeurs dans l'ordre de déclaration. « Défini par » nomme le code qui appelle Move/Fail ; « Observable » indique si GetStatusAsync/WaitForAsync peut le voir en pratique.

#ÉtatDéfini parObservableSignification
0CreatedStartSessionAsync (valeur initiale de la session active)BrièvementLa session est enregistrée ; rien ne s'est encore produit.
1ValidatingPlatformpersonneNonDéclaré, jamais défini par le service actuel (la validation de la plateforme a lieu dans PlanSessionAsync, qui n'a pas d'état de session).
2PlanningpersonneNonDéclaré, jamais défini (la planification a lieu avant qu'une session existe ; la nouvelle planification dans StartSessionAsync précède elle aussi l'enregistrement).
3AcquiringInstallationLockStartAsyncOuiLe bail d'installation est en cours d'acquisition. Échec : OL_E_INSTALLATION_BUSY.
4RecoveringPreviousTransactionStartAsyncOuiUn journal.json en attente est restauré avant la lecture de l'installation active ; la closure du plugin permanent est validée (plugin.integrity.reference), le hash de Omsi.exe est calculé, les ressources de l'écran de démarrage sont initialisées, un closecheck obsolète est supprimé. Échecs : OL_E_PERMANENT_PLUGIN_*, OL_E_SPLASH_*, OL_E_ITX_*, OL_E_CLOSECHECK_REMOVE_FAILED, OL_E_RECOVERY_*, OL_E_INSTALLATION_BUSY (processus journalisé encore actif).
5SnapshottingStartAsyncPratiquement nonDéfini immédiatement avant ApplyingConfiguration sans aucun await entre les deux ; l'instantané lui-même est pris dans ApplyAsync. Transitoire et non observable.
6ApplyingConfigurationStartAsyncOuijournal.json est écrit (Prepared), les originaux sont sauvegardés, les overlays écrits, les suppressions de session effectuées (Applied). Échecs : OL_E_UNKNOWN_SETTING, OL_E_SETTING_NOT_WRITABLE, OL_E_INVALID_SETTING_VALUE, erreurs d'E/S.
7DeployingRuntimeStartAsyncOuiÉtat du journal RuntimeDeployed. Aucun fichier n'est déployé : la closure du plugin est permanente.
8CreatingStartupHandoffStartAsyncOuiLe handoff (OmsiLaunch.Handoff.<id>), le slot de télémétrie (OmsiLaunch.Telemetry.<id>) et la boîte aux lettres runtime (OmsiLaunch.Runtime.<id>) existent ; état du journal HandoffCreated.
9StartingProcessStartAsyncOuiCreateProcessW pour <root>\Omsi.exe avec le répertoire de travail <root>. Échecs : OL_E_PROCESS_START_FAILED, OL_E_PROCESS_CREATION_TIME_FAILED.
10WaitingForPluginStartAsyncOuiLe processus existe, process.started est enregistré, le journal est à ProcessStarted, le superviseur est démarré et StartSessionAsync retourne.
11PluginBootstrapApplyTelemetry sur plugin.startedOuiLe plugin permanent a lu un handoff valide pour cette session. À partir d'ici, un timeout donne OL_E_STARTUP_TIMEOUT au lieu de OL_E_PLUGIN_NOT_LOADED.
12StartingWorldApplyTelemetry sur world.startingOui (NEW_MAP uniquement)Le plugin a invoqué le démarrage natif NEW_MAP sur le thread d'interface d'OMSI. Les situations enregistrées émettent world.situation.starting, qui n'est pas mappé, de sorte qu'une session SAVED_SITUATION passe directement de PluginBootstrap à Running.
13EnteringGameplaypersonneNonDéclaré, jamais défini : gameplay.entered fait passer la session directement à Running.
14RunningApplyTelemetry sur gameplay.enteredOuiLe jeu est atteint. ExecuteRuntimeAsync est autorisé ; le propriétaire CLI ouvre le plan de contrôle local ; la zone de notification affiche une session en cours.
15ProcessExitedSuperviseAsyncOui (sessions réussies uniquement)OMSI s'est terminé (naturellement ou par arrêt forcé), le journal est à ProcessExited.
16RestoringSuperviseAsync (et le chemin d'échec du démarrage)Oui (sessions réussies uniquement)Chaque fichier appartenant à la session est restauré à partir de sa sauvegarde vérifiée ; les artefacts de session sont supprimés.
17CleaningRuntimeSuperviseAsyncOui (sessions réussies uniquement)Restauration vérifiée, journal et sauvegardes supprimés ; les stores runtime sont sur le point d'être libérés.
18CompletedSuperviseAsync (finally)Oui, terminalStores libérés, boîte aux lettres fermée, bail libéré, aucun échec enregistré.
19FailedLiveSession.Fail depuis StartAsync, SuperviseAsync, ApplyTelemetryOui, terminalUn diagnostic d'échec a été enregistré. L'état est persistant : les appels Move ultérieurs sont ignorés, de sorte qu'une session en échec n'affiche jamais ProcessExited/Restoring/CleaningRuntime/Completed, même si l'arrêt forcé et la restauration ont quand même lieu.

États terminaux : Completed et Failed. Après l'un ou l'autre, WaitForAsync retourne immédiatement et CloseAsync retourne sans demander d'arrêt.

Vérification de « jamais défini » : une recherche dans le code source de SessionState.ValidatingPlatform, SessionState.Planning et SessionState.EnteringGameplay ne trouve que la déclaration de l'enum ; SessionState.Snapshotting apparaît une seule fois, immédiatement suivi de Move(SessionState.ApplyingConfiguration).

Phase de démarrage (StartSessionAsync)#

  1. Rejeter un plan non exécutable (OL_E_PLAN_NOT_RUNNABLE), replanifier la spécification (nouveau calcul du hash de Omsi.exe, nouvelle résolution du contenu, nouvelle vérification de la closure du plugin) et rejeter à nouveau si elle n'est plus exécutable. Enregistrer la session active (Created).
  2. Créer la trace de l'hôte <root>\.omsilaunch\diagnostics\<sessionId>-host.log (les anciens fichiers préfixés par une session au-delà des 50 sessions les plus récentes sont supprimés). Rejeter StartupTimeoutSeconds en dehors de 1..600 (ArgumentOutOfRangeException ; la session est désenregistrée).
  3. AcquiringInstallationLock → bail. RecoveringPreviousTransaction → récupérer un journal en attente (un journal antérieur aux empreintes qui ne peut pas prouver la propriété est différé et retenté une fois que les overlays de cette session existent), valider la closure du plugin permanent, calculer le hash de l'exécutable, supprimer un closecheck obsolète, construire la transaction (overlays : correctifs de options.cfg, BMP gérés de l'écran de démarrage, Texture\standard.itx ; suppressions : cibles ITX, Texture\standard.ipr, closecheck lorsqu'il n'existe pas).
  4. Snapshotting → ApplyingConfiguration → DeployingRuntime → CreatingStartupHandoff → StartingProcess → WaitingForPlugin, puis la tâche du superviseur démarre et le handle est renvoyé.
  5. Toute exception aux étapes 3–4 est interceptée : la session passe à Failed avec OL_E_START_SESSION (message interne), un processus créé est arrêté et attendu, les stores sont libérés, la transaction est restaurée (OL_E_RESTORE_FAILED en cas d'échec) ou, si la fin d'OMSI n'a pas pu être confirmée, laissée en attente avec OL_E_RESTORE_DEFERRED ; le bail est libéré. StartSessionAsync renvoie quand même le handle dans ce cas ; lisez GetStatusAsync.

La nouvelle planification conserve le SessionId de l'appelant, de sorte que l'identifiant du handle est égal à plan.SessionId.

Supervision (SuperviseAsync)#

Le superviseur s'exécute dans une tâche du pool de threads et boucle toutes les 100 ms jusqu'à ce qu'OMSI se soit terminé ou qu'un arrêt ait été demandé :

  1. Lire le dernier échantillon de télémétrie (un slot à dernière valeur avec une séquence de producteur ; les échantillons incohérents sont ignorés ; des événements consécutifs identiques restent distincts car la séquence diffère). Chaque nouvel échantillon est ajouté à RuntimeEvents et mappé par ApplyTelemetry.
  2. Si la session est Failed, quitter la boucle.
  3. Si la session n'est pas encore Running et que l'échéance (StartupTimeoutSeconds après l'entrée du superviseur) est dépassée : Fail avec OL_E_STARTUP_TIMEOUT lorsque PluginBootstrap a été atteint, sinon OL_E_PLUGIN_NOT_LOADED ; quitter la boucle.

Après la boucle : si OMSI s'est terminé avant Running et qu'aucun échec n'a été enregistré, Fail avec OL_E_PROCESS_EXITED_EARLY. Ensuite, que la session ait échoué ou non : arrêter OMSI s'il est encore actif, attendre sa fin, marquer le journal ProcessExited, passer à ProcessExited, restaurer (Restoring → CleaningRuntime) ou enregistrer OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED, libérer le handle de processus, le handoff, le slot de télémétrie et la boîte aux lettres runtime, libérer le bail, et passer à Completed sauf si l'état est Failed. Une erreur dans le superviseur lui-même est enregistrée comme OL_E_PROCESS_SUPERVISION (les problèmes de nettoyage comme OL_E_PROCESS_CLEANUP_FAILED) et le même chemin d'arrêt/restauration s'exécute.

Comme Failed est persistant, la seule preuve qu'une session en échec a été restaurée est l'absence de OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED dans ses diagnostics (et l'absence de journal.json) ; les notes de restauration (restore.session-artifact-removed, OL_W_RESTORE_FOREIGN_FILE_RETAINED) apparaissent dans les deux cas.

Événements de télémétrie#

Le plugin publie des échantillons JSON { "name": ..., "data": {...} } dans le slot de télémétrie ; l'hôte enregistre chaque nouvel échantillon comme un RuntimeEvent(Type = name, TimestampUtc = host receipt time, Sequence, Data).

EventÉmis parAction de l'hôte
plugin.started (session_id)PluginRuntime.Start après la lecture d'un handoff valideMove(PluginBootstrap) ; PluginStarted = true
plugin.handoff.invalidPluginRuntime.Start : pas de OMSILAUNCH_HANDOFF_NAME, handoff illisible ou invérifiableFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)
plugin.request.unsupportedPluginRuntime.Start : le handoff demande un mode de monde autre que NEW_MAP/SAVED_SITUATION, un démarrage non headless, un véhicule du joueur, des modes de date/heure, ou une identité de situation videFail(OL_E_CAPABILITY_UNAVAILABLE)
plugin.build.invalidPluginRuntime.Start : la validation du build dans le processus a échouéFail(OL_E_BUILD_VALIDATION_FAILED)
plugin.build.validatedPluginRuntime.Startenregistré uniquement
headless.arm.failedPluginRuntime.Start : le hook natif de démarrage headless n'a pas pu être arméFail(OL_E_HEADLESS_ARM_FAILED)
headless.armedPluginRuntime.Startenregistré uniquement
internet-textures.suppressed / internet-textures.suppression.failedCurrentDnneAdapter.PluginStart lorsque InternetTextures.Mode vaut Disabledenregistré uniquement
world.starting (map, presented_index, entrypoint_identity)PluginRuntime.ConsumePendingWorld (NEW_MAP)Move(StartingWorld)
world.waiting-native-ready (native_status 3 ou 4)NEW_MAP : OMSI n'est pas encore prêt ; le démarrage est retenté au prochain tick du timer d'interfaceenregistré uniquement
world.loaded, world.entrypoint.selected (presented_index, raw_index, presented_label, raw_label)chemin de réussite NEW_MAPenregistré uniquement
world.failed (native_status)NEW_MAP : le démarrage natif a renvoyé un échecFail(OL_E_WORLD_START_FAILED)
world.situation.starting, world.situation.loaded (situation)chemin SAVED_SITUATIONenregistré uniquement (aucun changement d'état)
world.situation.failed (native_status, situation)SAVED_SITUATION : le démarrage natif a renvoyé un échecFail(OL_E_SITUATION_LOAD_FAILED)
gameplay.entered (NEW_MAP : champs de sélection du point d'entrée ou entrypoint_diagnostics = unavailable ; SAVED_SITUATION : situation)fin du démarrage du mondeMove(Running)
d3d.ready, d3d.lost, d3d.resetting, d3d.restored, d3d.stopped (state, generation, execution_thread_id, live_textures)CurrentRuntimeControl.PollLifecycle dès qu'une opération d3d.* a activé la sondeenregistré uniquement
camera.lock.degraded (code)CurrentRuntimeControl.PollLifecycle lorsque la réapplication d'un camera.lock actif lève une exception (signalé une fois par erreur distincte)enregistré uniquement
JSON invaliden'importe lequelFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)

Réserves : le slot contient un seul échantillon, de sorte que des événements émis au cours d'un même cycle d'interrogation de 100 ms de l'hôte peuvent être perdus (le plugin supprime les événements de cycle de vie pendant 2 s après gameplay.entered et ne publie jamais d'événement D3D dans le tick qui publie gameplay.entered, afin que la frontière Running ne soit pas manquée). RuntimeEvents conserve les 256 événements les plus récents ; les plus anciens sont abandonnés. Ce n'est pas un journal sans perte. Lisez les événements via GetStatusAsync, session.events sur le plan de contrôle, ou events read|watch dans la CLI.

Timeout de démarrage#

ÉlémentValeur
SourceLaunchSpec.Behavior.StartupTimeoutSeconds (180 par défaut ; 1..600 ; CLI /startup-timeout, profil behavior.startup-timeout).
Début du décompteLorsque la tâche du superviseur entre dans sa boucle (après le renvoi du handle).
Expiration avant PluginBootstrapFailed avec OL_E_PLUGIN_NOT_LOADED.
Expiration après PluginBootstrap, avant RunningFailed avec OL_E_STARTUP_TIMEOUT.
Après RunningAucun timeout ne s'applique ; la session dure jusqu'à ce qu'OMSI se termine ou qu'un arrêt soit demandé.
Propriétaire CLIAttend Running pendant StartupTimeoutSeconds + 5 secondes ; en cas d'échec, affiche l'état, affiche sous OmsiLaunchW.exe une boîte de dialogue contenant le dernier diagnostic OL_E_ (code de repli OL_E_SESSION_START_FAILED), et se termine avec 1 après CloseAsync.

ShutdownTimeoutSeconds est transporté dans la spécification mais n'est pas utilisé : il n'y a pas d'attente d'arrêt.

Sémantique de l'arrêt#

Toute demande d'arrêt est la même demande canonique : StopAsync(handle) depuis l'API, CloseAsync sur une session non terminale, session.stop sur le plan de contrôle local (lié à l'identifiant de la session active), « End session » (Terminer la session) dans la zone de notification, Ctrl+C ou la fermeture de la console dans le propriétaire CLI, et la fin de /observe-seconds.

ÉtapeDétail
1StopRequested est défini sur la session active ; l'appelant retourne immédiatement.
2En moins de 100 ms, le superviseur quitte sa boucle et appelle TerminateProcess(Omsi.exe, 1). Il s'agit d'un arrêt forcé : la routine d'arrêt d'OMSI ne s'exécute pas, OMSI ne réécrit pas options.cfg, aucune boîte de dialogue d'enregistrement n'apparaît. C'est délibéré, afin qu'OMSI ne puisse pas écraser les fichiers que la transaction s'apprête à restaurer.
3Le superviseur attend la fin du processus, enregistre ProcessExited, restaure exactement chaque fichier appartenant à la session (y compris le marqueur closecheck écrit par OMSI pendant la session, qui devient une note restore.session-artifact-removed), supprime le journal et les sauvegardes, libère les stores runtime (les appels ultérieurs à ExecuteRuntimeAsync lèvent OL_E_RUNTIME_CHANNEL_CLOSED ou OL_E_SESSION_NOT_RUNNING), libère le bail et passe à Completed.
Fin naturelleSi OMSI se termine de lui-même après Running (l'utilisateur ferme OMSI), le même chemin s'exécute sans arrêt forcé et la session se termine normalement. Avant Running, c'est OL_E_PROCESS_EXITED_EARLY.
Arrêt coopératifNon implémenté. L'envoi de WM_CLOSE suivi d'une attente de ShutdownTimeoutSeconds n'est pas implémenté (décision produit ; OMSI a ignoré WM_CLOSE envoyé à sa fenêtre principale lors de la série de clôture runtime, L05b) (état de la validation à l'exécution).
État côté runtimeTout ce qui est modifié via des opérations runtime (horloge, caméra, véhicules créés en cours de session, variables de script, textures D3D) est un état interne au processus et disparaît avec lui ; il n'est jamais restauré ni persisté.

Sémantique de WaitForAsync#

SituationRésultat
La session atteint l'état demandéRenvoie l'état avec State == requested.
La session atteint d'abord un état terminalRetourne immédiatement avec Completed ou Failed (consultez Diagnostics).
Le timeout expireRenvoie l'état actuel (aucune exception). Comparez State avec ce que vous avez demandé.
L'état demandé est déjà dépassé (ou jamais défini : ValidatingPlatform, Planning, EnteringGameplay, en pratique Snapshotting)Attend jusqu'à un état terminal ou jusqu'au timeout.
L'appelant annuleOperationCanceledException.
Handle inconnu ou ferméKeyNotFoundException.

L'intervalle d'interrogation est de 100 ms ; les transitions observées ont donc jusqu'à 100 ms de retard sur les transitions réelles.

Garanties du cycle de vie du propriétaire (CLI)#

OwnerSession.RunAsync dans tools/OmsiLaunch.Cli/Program.cs est le propriétaire de référence.

GarantieDétail
Propriétaire uniqueAvant de démarrer, la CLI sonde le pipe de contrôle ; si un propriétaire répond, elle refuse avec OL_E_SESSION_ALREADY_ACTIVE (code de sortie 7). Le bail applique la même règle entre processus.
Chaque chemin de sortie atteint CloseAsyncÀ partir de StartSessionAsync, les exceptions, Ctrl+C (CancelKeyPress), la fermeture de la console ou la déconnexion (ProcessExit : l'arrêt est demandé et le propriétaire attend jusqu'à 4 s Completed ; ce qui reste est récupéré par le journal au démarrage suivant), l'arrêt depuis la zone de notification, session.stop sur le plan de contrôle, l'expiration de /observe-seconds et la fin naturelle aboutissent tous au bloc finally qui libère le plan de contrôle et la zone de notification et attend CloseAsync.
/observe-seconds est une borne supérieureLes demandes d'arrêt depuis la zone de notification ou le plan de contrôle peuvent toujours terminer la session plus tôt.
Plan de contrôle uniquement pendant RunningLe point de terminaison du canal nommé (named pipe) est créé après Running (et après d'éventuels lots de validation INTERNAL) et libéré avant CloseAsync ; les clients obtiennent OL_E_NO_ACTIVE_SESSION à tout autre moment.
Code de sortie0 lorsque l'état final est Completed, 1 lorsqu'il est Failed ou que le jeu n'a pas été atteint, 8 lorsqu'une récupération demandée n'a pas abouti (codes de sortie).
DiagnosticsTrace de l'hôte et artefacts des opérations runtime sous <root>\.omsilaunch\diagnostics, journal de la zone de notification tray-host.log ; aucune donnée ne quitte la machine.

Les intégrateurs qui écrivent leur propre propriétaire doivent reproduire les deux premières garanties : un seul StartSessionAsync à la fois par installation, et CloseAsync sur chaque chemin.

Carte des échecs#

PhaseÉtat lors de l'échecDiagnostics que vous verrez
Planaucun (pas de session)OL_E_PLAN_NOT_RUNNABLE levé par StartSessionAsync ; les propres codes OL_E_ du plan (validation de LaunchSpec).
Démarrage (du bail à la création du processus)FailedOL_E_START_SESSION avec le code interne ; éventuellement OL_E_PROCESS_CLEANUP_FAILED, OL_E_RESTORE_DEFERRED, OL_E_RESTORE_FAILED.
Amorçage du pluginFailedOL_E_PLUGIN_NOT_LOADED, OL_E_PLUGIN_PROTOCOL_MISMATCH, OL_E_CAPABILITY_UNAVAILABLE, OL_E_BUILD_VALIDATION_FAILED, OL_E_HEADLESS_ARM_FAILED.
Démarrage du mondeFailedOL_E_WORLD_START_FAILED, OL_E_SITUATION_LOAD_FAILED, OL_E_STARTUP_TIMEOUT, OL_E_PROCESS_EXITED_EARLY.
En cours d'exécutionFailed uniquement en cas d'erreur du superviseurOL_E_PROCESS_SUPERVISION ; les erreurs des opérations runtime ne font jamais échouer la session.
Arrêt forcé et restaurationFailedOL_E_RESTORE_FAILED, OL_E_RESTORE_DEFERRED, OL_E_PROCESS_CLEANUP_FAILED.

Chaque chemin d'échec tente quand même l'arrêt forcé et la restauration ; un journal restant est récupéré au démarrage suivant ou par RecoverPendingAsync / /recover (transactions et récupération).