Ciclo di vita della sessione

Documentazione della versione v0.1.0-beta.3Vedi il sorgente su GitHub

Traduzione della pagina originale in inglese di OmsiLaunch 0.1.0-beta3. La pagina inglese è normativa: in caso di differenze prevalgono la pagina inglese e il codice.

Questa pagina descrive come una sessione OmsiLaunch attraversa SessionState da Created a Completed o Failed: quale componente imposta ciascuno stato, quali eventi di telemetria del plugin guidano le transizioni, come funziona il timeout di avvio, che cosa significa l'arresto (terminazione forzata), che cosa restituisce WaitForAsync, quali stati sono terminali, quali stati non sono mai o quasi mai osservabili e quali garanzie offre l'owner della CLI. Tutto il contenuto è tratto da OmsiLaunchService.StartAsync, SuperviseAsync e ApplyTelemetry (src/OmsiLaunch.Core/OmsiLaunchService.cs), PluginRuntime (src/OmsiLaunch.Plugin/PluginRuntime.cs) e OwnerSession (tools/OmsiLaunch.Cli/Program.cs).

Pagine correlate: API pubblica, codici di errore, transazioni e recupero, plugin permanente, controllo runtime, piano di controllo locale, area di notifica di Windows, riferimento CLI, stato della validazione a runtime, la directory .omsilaunch.

Panoramica#

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

Riferimento di SessionState#

Valori nell'ordine di dichiarazione. «Impostato da» indica il codice che chiama Move/Fail; «Osservabile» indica se GetStatusAsync/WaitForAsync possono vederlo in pratica.

#StatoImpostato daOsservabileSignificato
0CreatedStartSessionAsync (valore iniziale della sessione attiva)BrevementeLa sessione è registrata; non è ancora successo nulla.
1ValidatingPlatformnessunoNoDichiarato, mai impostato dal servizio attuale (la validazione della piattaforma avviene in PlanSessionAsync, che non ha stato di sessione).
2PlanningnessunoNoDichiarato, mai impostato (la pianificazione avviene prima che esista una sessione; anche la ripianificazione in StartSessionAsync precede la registrazione).
3AcquiringInstallationLockStartAsyncSìÈ in corso l'acquisizione del lease dell'installazione. Errore: OL_E_INSTALLATION_BUSY.
4RecoveringPreviousTransactionStartAsyncSìUn journal.json in sospeso viene ripristinato prima che l'installazione attiva venga letta; la chiusura del plugin permanente viene validata (plugin.integrity.reference), viene calcolato l'hash di Omsi.exe, vengono predisposti gli asset della splash screen e viene rimosso un closecheck obsoleto. Errori: OL_E_PERMANENT_PLUGIN_*, OL_E_SPLASH_*, OL_E_ITX_*, OL_E_CLOSECHECK_REMOVE_FAILED, OL_E_RECOVERY_*, OL_E_INSTALLATION_BUSY (processo registrato nel journal ancora attivo).
5SnapshottingStartAsyncIn pratica noImpostato immediatamente prima di ApplyingConfiguration senza alcun await intermedio; lo snapshot vero e proprio viene acquisito all'interno di ApplyAsync. Transitorio e non osservabile.
6ApplyingConfigurationStartAsyncSìViene scritto journal.json (Prepared), viene eseguito il backup degli originali, vengono scritti gli overlay e rimossi i file oggetto di eliminazione di sessione (Applied). Errori: OL_E_UNKNOWN_SETTING, OL_E_SETTING_NOT_WRITABLE, OL_E_INVALID_SETTING_VALUE, errori di I/O.
7DeployingRuntimeStartAsyncSìStato del journal RuntimeDeployed. Nessun file viene distribuito: la chiusura del plugin è permanente.
8CreatingStartupHandoffStartAsyncSìL'handoff (OmsiLaunch.Handoff.<id>), lo slot di telemetria (OmsiLaunch.Telemetry.<id>) e la mailbox runtime (OmsiLaunch.Runtime.<id>) esistono; stato del journal HandoffCreated.
9StartingProcessStartAsyncSìCreateProcessW per <root>\Omsi.exe con directory di lavoro <root>. Errori: OL_E_PROCESS_START_FAILED, OL_E_PROCESS_CREATION_TIME_FAILED.
10WaitingForPluginStartAsyncSìIl processo esiste, process.started è registrato, il journal è ProcessStarted, il supervisore è avviato e StartSessionAsync restituisce il controllo.
11PluginBootstrapApplyTelemetry su plugin.startedSìIl plugin permanente ha letto un handoff valido per questa sessione. Da qui in poi un timeout è OL_E_STARTUP_TIMEOUT anziché OL_E_PLUGIN_NOT_LOADED.
12StartingWorldApplyTelemetry su world.startingSì (solo NEW_MAP)Il plugin ha invocato l'avvio nativo NEW_MAP sul thread dell'interfaccia utente di OMSI. Le situazioni salvate emettono world.situation.starting, che non è mappato, quindi una sessione SAVED_SITUATION passa da PluginBootstrap direttamente a Running.
13EnteringGameplaynessunoNoDichiarato, mai impostato: gameplay.entered porta la sessione direttamente a Running.
14RunningApplyTelemetry su gameplay.enteredSìGameplay raggiunto. ExecuteRuntimeAsync è consentito; l'owner della CLI apre il piano di controllo locale; l'area di notifica mostra una sessione in esecuzione.
15ProcessExitedSuperviseAsyncSì (solo sessioni riuscite)OMSI è terminato (naturalmente o per terminazione forzata), il journal è ProcessExited.
16RestoringSuperviseAsync (e il percorso di errore di avvio)Sì (solo sessioni riuscite)Ogni file di proprietà della sessione viene ripristinato dal relativo backup verificato; gli artefatti di sessione vengono rimossi.
17CleaningRuntimeSuperviseAsyncSì (solo sessioni riuscite)Ripristino verificato, journal e backup rimossi; gli store runtime stanno per essere rilasciati.
18CompletedSuperviseAsync (finally)Sì, terminaleStore rilasciati, mailbox chiusa, lease rilasciato, nessun errore registrato.
19FailedLiveSession.Fail da StartAsync, SuperviseAsync, ApplyTelemetrySì, terminaleÈ stata registrata una voce di diagnostica di errore. Lo stato è persistente: le chiamate Move successive vengono ignorate, quindi una sessione fallita non mostra mai ProcessExited/Restoring/CleaningRuntime/Completed, anche se la terminazione e il ripristino vengono comunque eseguiti.

Stati terminali: Completed e Failed. Dopo uno qualsiasi dei due, WaitForAsync restituisce immediatamente e CloseAsync restituisce senza richiedere un arresto.

Verifica di «mai impostato»: una ricerca nel codice sorgente di SessionState.ValidatingPlatform, SessionState.Planning e SessionState.EnteringGameplay trova solo la dichiarazione dell'enum; SessionState.Snapshotting compare una sola volta, seguito immediatamente da Move(SessionState.ApplyingConfiguration).

Fase di avvio (StartSessionAsync)#

  1. Rifiutare un piano non eseguibile (OL_E_PLAN_NOT_RUNNABLE), ripianificare la specifica (ricalcolando l'hash di Omsi.exe, risolvendo di nuovo i contenuti, ricontrollando la chiusura del plugin) e rifiutarla di nuovo se non è più eseguibile. Registrare la sessione attiva (Created).
  2. Creare la traccia dell'host <root>\.omsilaunch\diagnostics\<sessionId>-host.log (i file più vecchi con prefisso di sessione oltre le 50 sessioni più recenti vengono eliminati). Rifiutare StartupTimeoutSeconds al di fuori di 1..600 (ArgumentOutOfRangeException; la registrazione della sessione viene annullata).
  3. AcquiringInstallationLock → lease. RecoveringPreviousTransaction → recuperare un journal in sospeso (un journal precedente all'introduzione dell'impronta, che non può dimostrare la proprietà, viene rinviato e ritentato una volta che gli overlay di questa sessione esistono), validare la chiusura del plugin permanente, calcolare l'hash dell'eseguibile, rimuovere un closecheck obsoleto, costruire la transazione (overlay: patch di options.cfg, BMP della splash screen gestita, Texture\standard.itx; eliminazioni: destinazioni ITX, Texture\standard.ipr, closecheck quando non esiste).
  4. Snapshotting → ApplyingConfiguration → DeployingRuntime → CreatingStartupHandoff → StartingProcess → WaitingForPlugin, quindi il task del supervisore si avvia e l'handle viene restituito.
  5. Qualsiasi eccezione nei passaggi 3–4 viene intercettata: la sessione passa a Failed con OL_E_START_SESSION (messaggio interno), un processo eventualmente creato viene terminato e atteso, gli store vengono rilasciati, la transazione viene ripristinata (OL_E_RESTORE_FAILED in caso di errore) oppure, se non è stato possibile confermare l'uscita di OMSI, lasciata in sospeso con OL_E_RESTORE_DEFERRED; il lease viene rilasciato. Anche in questo caso StartSessionAsync restituisce l'handle; leggere GetStatusAsync.

La ripianificazione mantiene il SessionId del chiamante, quindi l'id nell'handle è uguale a plan.SessionId.

Supervisione (SuperviseAsync)#

Il supervisore viene eseguito su un task del thread pool ed esegue un ciclo ogni 100 ms finché OMSI non è terminato o non è stato richiesto un arresto:

  1. Leggere il campione di telemetria più recente (uno slot a valore più recente con una sequenza del produttore; i campioni incoerenti vengono scartati; eventi consecutivi identici sono distinti perché la sequenza differisce). Ogni nuovo campione viene aggiunto a RuntimeEvents e mappato da ApplyTelemetry.
  2. Se la sessione è Failed, uscire dal ciclo.
  3. Se la sessione non è ancora Running e la scadenza (StartupTimeoutSeconds dopo l'ingresso nel supervisore) è trascorsa: Fail con OL_E_STARTUP_TIMEOUT se è stato raggiunto PluginBootstrap, altrimenti OL_E_PLUGIN_NOT_LOADED; uscire dal ciclo.

Dopo il ciclo: se OMSI è terminato prima di Running e non è stato registrato alcun errore, Fail con OL_E_PROCESS_EXITED_EARLY. Quindi, indipendentemente dal fatto che la sessione sia fallita o no: terminare OMSI se è ancora attivo, attenderne l'uscita, marcare il journal come ProcessExited, passare a ProcessExited, ripristinare (Restoring → CleaningRuntime) oppure registrare OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED, rilasciare l'handle del processo, l'handoff, lo slot di telemetria e la mailbox runtime, rilasciare il lease e passare a Completed a meno che lo stato non sia Failed. Un guasto all'interno del supervisore stesso viene registrato come OL_E_PROCESS_SUPERVISION (i problemi di pulizia come OL_E_PROCESS_CLEANUP_FAILED) e viene eseguito lo stesso percorso di terminazione/ripristino.

Poiché Failed è persistente, l'unica evidenza che una sessione fallita è stata ripristinata è l'assenza di OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED nella sua diagnostica (e l'assenza di journal.json); le note di ripristino (restore.session-artifact-removed, OL_W_RESTORE_FOREIGN_FILE_RETAINED) compaiono in entrambi i casi.

Eventi di telemetria#

Il plugin pubblica campioni JSON { "name": ..., "data": {...} } nello slot di telemetria; l'host registra ogni nuovo campione come RuntimeEvent(Type = name, TimestampUtc = host receipt time, Sequence, Data).

EventEmesso daAzione dell'host
plugin.started (session_id)PluginRuntime.Start dopo la lettura di un handoff validoMove(PluginBootstrap); PluginStarted = true
plugin.handoff.invalidPluginRuntime.Start: nessun OMSILAUNCH_HANDOFF_NAME, handoff illeggibile o non verificabileFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)
plugin.request.unsupportedPluginRuntime.Start: l'handoff richiede una modalità mondo diversa da NEW_MAP/SAVED_SITUATION, un avvio non headless, il veicolo del giocatore, modalità di data/ora o un'identità di situazione vuotaFail(OL_E_CAPABILITY_UNAVAILABLE)
plugin.build.invalidPluginRuntime.Start: la validazione del build nel processo non è riuscitaFail(OL_E_BUILD_VALIDATION_FAILED)
plugin.build.validatedPluginRuntime.Startsolo registrato
headless.arm.failedPluginRuntime.Start: non è stato possibile armare l'hook nativo di avvio headlessFail(OL_E_HEADLESS_ARM_FAILED)
headless.armedPluginRuntime.Startsolo registrato
internet-textures.suppressed / internet-textures.suppression.failedCurrentDnneAdapter.PluginStart quando InternetTextures.Mode è Disabledsolo registrato
world.starting (map, presented_index, entrypoint_identity)PluginRuntime.ConsumePendingWorld (NEW_MAP)Move(StartingWorld)
world.waiting-native-ready (native_status 3 o 4)NEW_MAP: OMSI non è ancora pronto; l'avvio viene ritentato al tick successivo del timer dell'interfaccia utentesolo registrato
world.loaded, world.entrypoint.selected (presented_index, raw_index, presented_label, raw_label)percorso di successo NEW_MAPsolo registrato
world.failed (native_status)NEW_MAP: l'avvio nativo ha restituito un erroreFail(OL_E_WORLD_START_FAILED)
world.situation.starting, world.situation.loaded (situation)percorso SAVED_SITUATIONsolo registrato (nessun cambio di stato)
world.situation.failed (native_status, situation)SAVED_SITUATION: l'avvio nativo ha restituito un erroreFail(OL_E_SITUATION_LOAD_FAILED)
gameplay.entered (NEW_MAP: campi di selezione del punto di ingresso oppure entrypoint_diagnostics = unavailable; SAVED_SITUATION: situation)fine dell'avvio del mondoMove(Running)
d3d.ready, d3d.lost, d3d.resetting, d3d.restored, d3d.stopped (state, generation, execution_thread_id, live_textures)CurrentRuntimeControl.PollLifecycle una volta che una qualsiasi operazione d3d.* ha attivato la sondasolo registrato
camera.lock.degraded (code)CurrentRuntimeControl.PollLifecycle quando la riapplicazione di un camera.lock attivo genera un'eccezione (segnalato una volta per ciascun errore distinto)solo registrato
JSON non validoqualsiasiFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)

Avvertenze: lo slot contiene un solo campione, quindi gli eventi emessi durante un unico polling dell'host di 100 ms possono andare persi (il plugin sopprime gli eventi del ciclo di vita per 2 s dopo gameplay.entered e non pubblica mai un evento D3D nello stesso tick in cui pubblica gameplay.entered, quindi il confine Running non viene perso). RuntimeEvents conserva i 256 eventi più recenti; quelli più vecchi vengono scartati. Non è un log senza perdite. Leggere gli eventi tramite GetStatusAsync, session.events sul piano di controllo oppure events read|watch nella CLI.

Timeout di avvio#

ElementoValore
OrigineLaunchSpec.Behavior.StartupTimeoutSeconds (predefinito 180; 1..600; CLI /startup-timeout, profilo behavior.startup-timeout).
Inizio del conteggioQuando il task del supervisore entra nel suo ciclo (dopo la restituzione dell'handle).
Scadenza prima di PluginBootstrapFailed con OL_E_PLUGIN_NOT_LOADED.
Scadenza dopo PluginBootstrap, prima di RunningFailed con OL_E_STARTUP_TIMEOUT.
Dopo RunningNon si applica alcun timeout; la sessione dura finché OMSI non termina o non viene richiesto un arresto.
Owner della CLIAttende StartupTimeoutSeconds + 5 secondi il raggiungimento di Running; in caso di errore stampa lo stato, con OmsiLaunchW.exe mostra una finestra di dialogo con l'ultima voce di diagnostica OL_E_ (codice di ripiego OL_E_SESSION_START_FAILED) ed esce con 1 dopo CloseAsync.

ShutdownTimeoutSeconds è presente nella specifica ma non viene utilizzato: non esiste alcuna attesa di chiusura.

Semantica dell'arresto#

Ogni richiesta di arresto è la stessa richiesta canonica: StopAsync(handle) dall'API, CloseAsync su una sessione non terminale, session.stop sul piano di controllo locale (vincolato all'id della sessione attiva), «End session» nell'area di notifica, Ctrl+C o la chiusura della console nell'owner della CLI e la fine di /observe-seconds.

PassaggioDettaglio
1StopRequested viene impostato sulla sessione attiva; il chiamante riprende immediatamente il controllo.
2Entro 100 ms il supervisore esce dal suo ciclo e chiama TerminateProcess(Omsi.exe, 1). Si tratta di una terminazione forzata: la routine di chiusura di OMSI non viene eseguita, OMSI non riscrive options.cfg, non compare alcuna finestra di dialogo di salvataggio. Ciò è intenzionale, affinché OMSI non possa sovrascrivere i file che la transazione sta per ripristinare.
3Il supervisore attende l'uscita del processo, registra ProcessExited, ripristina esattamente ogni file di proprietà della sessione (incluso il marcatore closecheck scritto da OMSI durante la sessione, che diventa una nota restore.session-artifact-removed), rimuove il journal e i backup, rilascia gli store runtime (le successive chiamate a ExecuteRuntimeAsync generano OL_E_RUNTIME_CHANNEL_CLOSED o OL_E_SESSION_NOT_RUNNING), rilascia il lease e passa a Completed.
Uscita naturaleSe OMSI termina da solo dopo Running (l'utente chiude OMSI), viene eseguito lo stesso percorso senza terminazione forzata e la sessione si completa normalmente. Prima di Running si ha OL_E_PROCESS_EXITED_EARLY.
Chiusura cooperativaNon implementata. L'invio di WM_CLOSE e l'attesa di ShutdownTimeoutSeconds non sono implementati (decisione di prodotto; OMSI ha ignorato WM_CLOSE inviato alla sua finestra principale nel round di chiusura runtime, L05b) (stato della validazione a runtime).
Stato lato runtimeTutto ciò che viene modificato tramite operazioni runtime (orologio, telecamera, veicoli generati, variabili di script, texture D3D) è stato interno al processo e scompare con il processo; non viene mai ripristinato né reso persistente.

Semantica di WaitForAsync#

SituazioneRisultato
La sessione raggiunge lo stato richiestoRestituisce lo stato con State == requested.
La sessione raggiunge prima uno stato terminaleRestituisce immediatamente con Completed o Failed (controllare Diagnostics).
Il timeout scadeRestituisce lo stato corrente (nessuna eccezione). Confrontare State con quanto richiesto.
Stato richiesto già superato (o mai impostato: ValidatingPlatform, Planning, EnteringGameplay, di fatto Snapshotting)Attende fino a uno stato terminale o al timeout.
Il chiamante annullaOperationCanceledException.
Handle sconosciuto o chiusoKeyNotFoundException.

L'intervallo di polling è di 100 ms, quindi le transizioni osservate sono in ritardo rispetto a quelle reali fino a 100 ms.

Garanzie del ciclo di vita dell'owner (CLI)#

OwnerSession.RunAsync in tools/OmsiLaunch.Cli/Program.cs è l'owner di riferimento.

GaranziaDettaglio
Owner unicoPrima dell'avvio, la CLI sonda la pipe di controllo; se un owner risponde, rifiuta con OL_E_SESSION_ALREADY_ACTIVE (uscita 7). Il lease impone la stessa regola tra processi diversi.
Ogni percorso di uscita raggiunge CloseAsyncA partire da StartSessionAsync, eccezioni, Ctrl+C (CancelKeyPress), chiusura della console/disconnessione (ProcessExit: viene richiesto l'arresto e l'owner attende fino a 4 s il raggiungimento di Completed; ciò che rimane viene recuperato tramite il journal all'avvio successivo), arresto dall'area di notifica, session.stop dal piano di controllo, scadenza di /observe-seconds e completamento naturale terminano tutti nel blocco finally, che rilascia il piano di controllo e l'area di notifica e attende CloseAsync.
/observe-seconds è un limite superioreLe richieste di arresto dall'area di notifica o dal piano di controllo terminano comunque la sessione prima.
Piano di controllo solo durante RunningL'endpoint della named pipe viene creato dopo Running (e dopo eventuali batch di validazione INTERNAL) e rilasciato prima di CloseAsync; negli altri momenti i client ricevono OL_E_NO_ACTIVE_SESSION.
Codice di uscita0 quando lo stato finale è Completed, 1 quando è Failed o il gameplay non è stato raggiunto, 8 quando un recupero richiesto non è stato completato (codici di uscita).
DiagnosticaTraccia dell'host e artefatti delle operazioni runtime in <root>\.omsilaunch\diagnostics, log dell'area di notifica tray-host.log; nessun dato lascia la macchina.

Gli integratori che scrivono un proprio owner devono riprodurre le prime due garanzie: un solo StartSessionAsync per installazione alla volta e CloseAsync su ogni percorso.

Mappa degli errori#

FaseStato in caso di erroreDiagnostica visibile
Pianonessuno (nessuna sessione)OL_E_PLAN_NOT_RUNNABLE generato da StartSessionAsync; i codici OL_E_ propri del piano (validazione di LaunchSpec).
Avvio (dal lease alla creazione del processo)FailedOL_E_START_SESSION con il codice interno; eventualmente OL_E_PROCESS_CLEANUP_FAILED, OL_E_RESTORE_DEFERRED, OL_E_RESTORE_FAILED.
Bootstrap del pluginFailedOL_E_PLUGIN_NOT_LOADED, OL_E_PLUGIN_PROTOCOL_MISMATCH, OL_E_CAPABILITY_UNAVAILABLE, OL_E_BUILD_VALIDATION_FAILED, OL_E_HEADLESS_ARM_FAILED.
Avvio del mondoFailedOL_E_WORLD_START_FAILED, OL_E_SITUATION_LOAD_FAILED, OL_E_STARTUP_TIMEOUT, OL_E_PROCESS_EXITED_EARLY.
RunningFailed solo in caso di guasti del supervisoreOL_E_PROCESS_SUPERVISION; gli errori delle operazioni runtime non fanno mai fallire la sessione.
Terminazione e ripristinoFailedOL_E_RESTORE_FAILED, OL_E_RESTORE_DEFERRED, OL_E_PROCESS_CLEANUP_FAILED.

Ogni percorso di errore tenta comunque la terminazione e il ripristino; un journal che rimane viene recuperato all'avvio successivo oppure da RecoverPendingAsync / /recover (transazioni e recupero).