Жизненный цикл сеанса

Документация версии v0.1.0-beta.3Исходник на GitHub

Перевод исходной страницы на английском языке для OmsiLaunch 0.1.0-beta3. Нормативной является английская страница: при расхождениях приоритет имеют английская страница и код.

На этой странице описано, как сеанс OmsiLaunch проходит через состояния SessionState от Created до Completed или Failed: какой компонент устанавливает каждое состояние, какие события телеметрии плагина вызывают переходы, как работает тайм-аут запуска, что означает остановка (принудительное завершение), что возвращает WaitForAsync, какие состояния являются терминальными, какие состояния никогда или почти никогда не наблюдаются и какие гарантии даёт владелец в CLI. Всё изложенное взято из OmsiLaunchService.StartAsync, SuperviseAsync и ApplyTelemetry (src/OmsiLaunch.Core/OmsiLaunchService.cs), PluginRuntime (src/OmsiLaunch.Plugin/PluginRuntime.cs) и OwnerSession (tools/OmsiLaunch.Cli/Program.cs).

Связанные страницы: публичный API, коды ошибок, транзакции и восстановление после сбоя, постоянный плагин, runtime-управление, локальная плоскость управления, трей Windows, справочник CLI, статус проверки в runtime, каталог .omsilaunch.

Обзор#

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

Справочник SessionState#

Значения приведены в порядке объявления. Столбец «Кем устанавливается» называет код, который вызывает Move/Fail; столбец «Наблюдаемость» показывает, могут ли GetStatusAsync/WaitForAsync увидеть это состояние на практике.

№СостояниеКем устанавливаетсяНаблюдаемостьЗначение
0CreatedStartSessionAsync (начальное значение активного сеанса)КратковременноСеанс зарегистрирован; ещё ничего не произошло.
1ValidatingPlatformникемНетОбъявлено, но текущий сервис его никогда не устанавливает (проверка платформы выполняется в PlanSessionAsync, у которого нет состояния сеанса).
2PlanningникемНетОбъявлено, никогда не устанавливается (планирование происходит до появления сеанса; повторное планирование в StartSessionAsync также предшествует регистрации).
3AcquiringInstallationLockStartAsyncДаЗахватывается аренда установки (lease). Ошибка: OL_E_INSTALLATION_BUSY.
4RecoveringPreviousTransactionStartAsyncДаНезавершённый journal.json восстанавливается до чтения активной установки; проверяется замыкание постоянного плагина (набор его файлов) (plugin.integrity.reference), вычисляется хеш Omsi.exe, подготавливаются ресурсы заставки, удаляется устаревший closecheck. Ошибки: OL_E_PERMANENT_PLUGIN_*, OL_E_SPLASH_*, OL_E_ITX_*, OL_E_CLOSECHECK_REMOVE_FAILED, OL_E_RECOVERY_*, OL_E_INSTALLATION_BUSY (процесс из журнала жив).
5SnapshottingStartAsyncПрактически нетУстанавливается непосредственно перед ApplyingConfiguration без await между ними; сам снимок (snapshot) делается внутри ApplyAsync. Кратковременное и ненаблюдаемое состояние.
6ApplyingConfigurationStartAsyncДаЗаписывается journal.json (Prepared), создаются резервные копии оригиналов, записываются overlay (временная замена файла на время сеанса), удаляются файлы, помеченные на удаление на время сеанса (Applied). Ошибки: OL_E_UNKNOWN_SETTING, OL_E_SETTING_NOT_WRITABLE, OL_E_INVALID_SETTING_VALUE, ошибки ввода-вывода.
7DeployingRuntimeStartAsyncДаСостояние журнала RuntimeDeployed. Никакие файлы не развёртываются: замыкание плагина постоянно.
8CreatingStartupHandoffStartAsyncДаСуществуют handoff (OmsiLaunch.Handoff.<id>), слот телеметрии (OmsiLaunch.Telemetry.<id>) и почтовый ящик (mailbox) runtime (OmsiLaunch.Runtime.<id>); состояние журнала HandoffCreated.
9StartingProcessStartAsyncДаCreateProcessW для <root>\Omsi.exe с рабочим каталогом <root>. Ошибки: OL_E_PROCESS_START_FAILED, OL_E_PROCESS_CREATION_TIME_FAILED.
10WaitingForPluginStartAsyncДаПроцесс существует, process.started записано, журнал в состоянии ProcessStarted, супервизор запущен, и StartSessionAsync возвращает управление.
11PluginBootstrapApplyTelemetry по plugin.startedДаПостоянный плагин прочитал корректный handoff для этого сеанса. С этого момента тайм-аут означает OL_E_STARTUP_TIMEOUT, а не OL_E_PLUGIN_NOT_LOADED.
12StartingWorldApplyTelemetry по world.startingДа (только NEW_MAP)Плагин вызвал нативный запуск NEW_MAP в UI-потоке OMSI. Сохранённые ситуации отправляют world.situation.starting, которое не сопоставлено ни с каким состоянием, поэтому сеанс SAVED_SITUATION переходит из PluginBootstrap сразу в Running.
13EnteringGameplayникемНетОбъявлено, никогда не устанавливается: gameplay.entered переводит сеанс прямо в Running.
14RunningApplyTelemetry по gameplay.enteredДаИгровой процесс достигнут. Разрешён ExecuteRuntimeAsync; владелец в CLI открывает локальную плоскость управления; трей показывает работающий сеанс.
15ProcessExitedSuperviseAsyncДа (только для успешных сеансов)OMSI завершилась (штатно или принудительно), журнал в состоянии ProcessExited.
16RestoringSuperviseAsync (и путь обработки ошибки запуска)Да (только для успешных сеансов)Каждый файл, которым владеет сеанс, восстанавливается из проверенной резервной копии; артефакты сеанса удаляются.
17CleaningRuntimeSuperviseAsyncДа (только для успешных сеансов)Восстановление проверено, журнал и резервные копии удалены; хранилища runtime вот-вот будут освобождены.
18CompletedSuperviseAsync (finally)Да, терминальноеХранилища освобождены, почтовый ящик закрыт, аренда освобождена, ошибка не зафиксирована.
19FailedLiveSession.Fail из StartAsync, SuperviseAsync, ApplyTelemetryДа, терминальноеЗафиксировано диагностическое сообщение об ошибке. Состояние «липкое»: последующие вызовы Move игнорируются, поэтому сеанс с ошибкой никогда не показывает ProcessExited/Restoring/CleaningRuntime/Completed, хотя завершение процесса и восстановление всё равно выполняются.

Терминальные состояния: Completed и Failed. После любого из них WaitForAsync возвращает управление немедленно, а CloseAsync возвращает управление, не запрашивая остановку.

Проверка утверждения «никогда не устанавливается»: поиск по кодовой базе SessionState.ValidatingPlatform, SessionState.Planning и SessionState.EnteringGameplay находит только объявление перечисления; SessionState.Snapshotting встречается один раз, и сразу за ним следует Move(SessionState.ApplyingConfiguration).

Этап запуска (StartSessionAsync)#

  1. Неисполнимый план отклоняется (OL_E_PLAN_NOT_RUNNABLE), спецификация планируется повторно (повторно вычисляется хеш Omsi.exe, заново разрешается контент, повторно проверяется замыкание плагина) и снова отклоняется, если она больше не исполнима. Регистрируется активный сеанс (Created).
  2. Создаётся трассировка хоста <root>\.omsilaunch\diagnostics\<sessionId>-host.log (более старые файлы с префиксом сеанса сверх 50 самых новых сеансов удаляются). StartupTimeoutSeconds вне диапазона 1..600 отклоняется (ArgumentOutOfRangeException; регистрация сеанса отменяется).
  3. AcquiringInstallationLock → аренда. RecoveringPreviousTransaction → восстанавливается незавершённый журнал (журнал, созданный до появления отпечатков и не способный доказать владение, откладывается и повторяется, когда overlay этого сеанса уже существуют), проверяется замыкание постоянного плагина, вычисляется хеш исполняемого файла, удаляется устаревший closecheck, строится транзакция (overlay: патчи options.cfg, BMP управляемой заставки, Texture\standard.itx; удаления: цели ITX, Texture\standard.ipr, closecheck, если он не существует).
  4. Snapshotting → ApplyingConfiguration → DeployingRuntime → CreatingStartupHandoff → StartingProcess → WaitingForPlugin, затем запускается задача супервизора и возвращается handle.
  5. Любое исключение на шагах 3–4 перехватывается: сеанс переходит в Failed с OL_E_START_SESSION (с внутренним сообщением), созданный процесс завершается и ожидается, хранилища освобождаются, транзакция восстанавливается (OL_E_RESTORE_FAILED при неудаче) или, если завершение OMSI не удалось подтвердить, остаётся незавершённой с OL_E_RESTORE_DEFERRED; аренда освобождается. В этом случае StartSessionAsync всё равно возвращает handle; состояние следует читать через GetStatusAsync.

Повторное планирование сохраняет SessionId вызывающей стороны, поэтому идентификатор в handle равен plan.SessionId.

Супервизия (SuperviseAsync)#

Супервизор работает в задаче пула потоков и повторяет цикл каждые 100 ms, пока OMSI не завершится или не будет запрошена остановка:

  1. Считывается последний образец телеметрии (слот последнего значения с последовательным номером производителя; «разорванные» образцы пропускаются; одинаковые последовательные события различаются, поскольку отличается номер последовательности). Каждый новый образец добавляется в RuntimeEvents и сопоставляется через ApplyTelemetry.
  2. Если сеанс в состоянии Failed, цикл завершается.
  3. Если сеанс ещё не в Running и крайний срок (StartupTimeoutSeconds после входа в супервизор) истёк: Fail с OL_E_STARTUP_TIMEOUT, если PluginBootstrap был достигнут, иначе OL_E_PLUGIN_NOT_LOADED; цикл завершается.

После цикла: если OMSI завершилась до Running и ошибка не была зафиксирована, выполняется Fail с OL_E_PROCESS_EXITED_EARLY. Затем, независимо от того, завершился ли сеанс с ошибкой: OMSI принудительно завершается, если она ещё жива, ожидается выход, журнал помечается ProcessExited, выполняется переход в ProcessExited, восстановление (Restoring → CleaningRuntime) или фиксация OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED, освобождаются дескриптор процесса, handoff, слот телеметрии и почтовый ящик runtime, освобождается аренда, и выполняется переход в Completed, если состояние не Failed. Сбой внутри самого супервизора фиксируется как OL_E_PROCESS_SUPERVISION (проблемы очистки — как OL_E_PROCESS_CLEANUP_FAILED), и выполняется тот же путь завершения процесса и восстановления.

Поскольку Failed — «липкое» состояние, единственное подтверждение того, что сеанс с ошибкой был восстановлен, — это отсутствие OL_E_RESTORE_FAILED/OL_E_RESTORE_DEFERRED в его диагностике (и отсутствие journal.json); примечания о восстановлении (restore.session-artifact-removed, OL_W_RESTORE_FOREIGN_FILE_RETAINED) появляются в обоих случаях.

События телеметрии#

Плагин публикует образцы JSON { "name": ..., "data": {...} } в слот телеметрии; хост записывает каждый новый образец как RuntimeEvent(Type = name, TimestampUtc = host receipt time, Sequence, Data).

EventКем отправляетсяДействие хоста
plugin.started (session_id)PluginRuntime.Start после чтения корректного handoffMove(PluginBootstrap); PluginStarted = true
plugin.handoff.invalidPluginRuntime.Start: нет OMSILAUNCH_HANDOFF_NAME, handoff нечитаем или не проходит проверкуFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)
plugin.request.unsupportedPluginRuntime.Start: handoff запрашивает режим мира, отличный от NEW_MAP/SAVED_SITUATION, запуск не в headless-режиме, транспортное средство игрока, режимы даты/времени или пустую идентичность ситуацииFail(OL_E_CAPABILITY_UNAVAILABLE)
plugin.build.invalidPluginRuntime.Start: проверка сборки внутри процесса не пройденаFail(OL_E_BUILD_VALIDATION_FAILED)
plugin.build.validatedPluginRuntime.Startтолько записывается
headless.arm.failedPluginRuntime.Start: не удалось взвести нативный хук headless-запускаFail(OL_E_HEADLESS_ARM_FAILED)
headless.armedPluginRuntime.Startтолько записывается
internet-textures.suppressed / internet-textures.suppression.failedCurrentDnneAdapter.PluginStart, когда InternetTextures.Mode равно Disabledтолько записывается
world.starting (map, presented_index, entrypoint_identity)PluginRuntime.ConsumePendingWorld (NEW_MAP)Move(StartingWorld)
world.waiting-native-ready (native_status 3 или 4)NEW_MAP: OMSI ещё не готова; запуск повторяется на следующем тике UI-таймератолько записывается
world.loaded, world.entrypoint.selected (presented_index, raw_index, presented_label, raw_label)Успешный путь NEW_MAPтолько записывается
world.failed (native_status)NEW_MAP: нативный запуск вернул ошибкуFail(OL_E_WORLD_START_FAILED)
world.situation.starting, world.situation.loaded (situation)Путь SAVED_SITUATIONтолько записывается (без смены состояния)
world.situation.failed (native_status, situation)SAVED_SITUATION: нативный запуск вернул ошибкуFail(OL_E_SITUATION_LOAD_FAILED)
gameplay.entered (NEW_MAP: поля выбора точки входа или entrypoint_diagnostics = unavailable; SAVED_SITUATION: situation)конец запуска мираMove(Running)
d3d.ready, d3d.lost, d3d.resetting, d3d.restored, d3d.stopped (state, generation, execution_thread_id, live_textures)CurrentRuntimeControl.PollLifecycle, после того как любая операция d3d.* активировала пробутолько записывается
camera.lock.degraded (code)CurrentRuntimeControl.PollLifecycle, когда повторное применение активной camera.lock выбрасывает исключение (сообщается один раз для каждой отдельной ошибки)только записывается
Некорректный JSONлюбойFail(OL_E_PLUGIN_PROTOCOL_MISMATCH)

Оговорки: слот хранит один образец, поэтому события, отправленные в пределах одного опроса хоста длительностью 100 ms, могут быть потеряны (плагин подавляет события жизненного цикла в течение 2 s после gameplay.entered и никогда не публикует событие D3D в том же тике, в котором публикуется gameplay.entered, поэтому граница Running не пропускается). RuntimeEvents хранит 256 самых последних событий; более старые отбрасываются. Это не лог без потерь. События следует читать через GetStatusAsync, session.events в плоскости управления или events read|watch в CLI.

Тайм-аут запуска#

ЭлементЗначение
ИсточникLaunchSpec.Behavior.StartupTimeoutSeconds (по умолчанию 180; 1..600; в CLI /startup-timeout, в профиле behavior.startup-timeout).
Начало отсчётаКогда задача супервизора входит в свой цикл (после возврата handle).
Истечение до PluginBootstrapFailed с OL_E_PLUGIN_NOT_LOADED.
Истечение после PluginBootstrap, до RunningFailed с OL_E_STARTUP_TIMEOUT.
После RunningТайм-аут не применяется; сеанс длится, пока OMSI не завершится или не будет запрошена остановка.
Владелец в CLIОжидает Running в течение StartupTimeoutSeconds + 5 секунд; при неудаче выводит статус, в OmsiLaunchW.exe показывает диалог с последним диагностическим сообщением OL_E_ (резервный код OL_E_SESSION_START_FAILED) и завершается с кодом 1 после CloseAsync.

ShutdownTimeoutSeconds передаётся в спецификации, но не используется: ожидания штатного завершения нет.

Семантика остановки#

Каждый запрос остановки — это один и тот же канонический запрос: StopAsync(handle) из API, CloseAsync для нетерминального сеанса, session.stop в локальной плоскости управления (привязан к идентификатору активного сеанса), пункт «End session» (завершить сеанс) в трее, Ctrl+C или закрытие консоли у владельца в CLI, а также окончание /observe-seconds.

ШагПодробности
1В активном сеансе устанавливается StopRequested; вызывающая сторона сразу получает управление обратно.
2В течение 100 ms супервизор выходит из цикла и вызывает TerminateProcess(Omsi.exe, 1). Это принудительное завершение: процедура завершения OMSI не выполняется, OMSI не перезаписывает options.cfg, диалог сохранения не появляется. Так сделано намеренно, чтобы OMSI не могла перезаписать файлы, которые транзакция собирается восстановить.
3Супервизор ожидает завершения процесса, фиксирует ProcessExited, точно восстанавливает каждый файл, которым владеет сеанс (включая маркер closecheck, записанный OMSI во время сеанса; он превращается в примечание restore.session-artifact-removed), удаляет журнал и резервные копии, освобождает хранилища runtime (последующие вызовы ExecuteRuntimeAsync выбрасывают OL_E_RUNTIME_CHANNEL_CLOSED или OL_E_SESSION_NOT_RUNNING), освобождает аренду и переходит в Completed.
Штатный выходЕсли OMSI завершается сама после Running (пользователь закрывает OMSI), выполняется тот же путь без принудительного завершения, и сеанс завершается нормально. До Running это OL_E_PROCESS_EXITED_EARLY.
Кооперативное завершениеНе реализовано. Отправка WM_CLOSE и ожидание ShutdownTimeoutSeconds не реализованы (решение по продукту; в раунде завершающей проверки в runtime OMSI проигнорировала WM_CLOSE, отправленное её главному окну, L05b) (статус проверки в runtime).
Состояние на стороне runtimeВсё, что изменено через runtime-операции (часы, камера, созданные ТС, переменные скриптов, текстуры D3D), является состоянием внутри процесса и исчезает вместе с процессом; оно никогда не восстанавливается и не сохраняется.

Семантика WaitForAsync#

СитуацияРезультат
Сеанс достигает запрошенного состоянияВозвращает статус с State == requested.
Сеанс раньше достигает терминального состоянияНемедленно возвращает Completed или Failed (проверьте Diagnostics).
Истекает тайм-аутВозвращает текущий статус (без исключения). Сравните State с запрошенным состоянием.
Запрошенное состояние уже пройдено (или никогда не устанавливается: ValidatingPlatform, Planning, EnteringGameplay, фактически Snapshotting)Ожидает терминального состояния или тайм-аута.
Вызывающая сторона отменяет операциюOperationCanceledException.
Неизвестный или закрытый handleKeyNotFoundException.

Интервал опроса — 100 ms, поэтому наблюдаемые переходы отстают от реальных не более чем на 100 ms.

Гарантии жизненного цикла владельца (CLI)#

OwnerSession.RunAsync в tools/OmsiLaunch.Cli/Program.cs — эталонный владелец.

ГарантияПодробности
Единственный владелецПеред запуском CLI опрашивает канал управления; если владелец отвечает, запуск отклоняется с OL_E_SESSION_ALREADY_ACTIVE (код выхода 7). Аренда обеспечивает то же правило между процессами.
Каждый путь выхода доходит до CloseAsyncНачиная с StartSessionAsync, исключения, Ctrl+C (CancelKeyPress), закрытие консоли или выход из системы (ProcessExit: запрашивается остановка, и владелец ждёт Completed до 4 s; всё оставшееся восстанавливается по журналу при следующем запуске), остановка из трея, session.stop в плоскости управления, истечение /observe-seconds и штатное завершение — все заканчиваются в блоке finally, который освобождает плоскость управления и трей и ожидает CloseAsync.
/observe-seconds — верхняя границаЗапросы остановки из трея или плоскости управления по-прежнему завершают сеанс раньше.
Плоскость управления только в состоянии RunningКонечная точка именованного канала (named pipe) создаётся после Running (и после всех пакетов проверки INTERNAL) и освобождается до CloseAsync; в остальное время клиенты получают OL_E_NO_ACTIVE_SESSION.
Код выхода0, если конечное состояние Completed; 1, если оно Failed или игровой процесс не был достигнут; 8, если запрошенное восстановление после сбоя не завершилось (коды выхода).
ДиагностикаТрассировка хоста и артефакты runtime-операций в <root>\.omsilaunch\diagnostics, лог трея tray-host.log; никакие данные не покидают компьютер.

Интеграторы, которые пишут собственного владельца, должны воспроизвести первые две гарантии: один StartSessionAsync на установку в каждый момент времени и CloseAsync на каждом пути.

Карта ошибок#

ЭтапСостояние при ошибкеДиагностика, которую вы увидите
Планнет (сеанса нет)OL_E_PLAN_NOT_RUNNABLE, выбрасываемое StartSessionAsync; собственные коды OL_E_ плана (проверка LaunchSpec).
Запуск (от аренды до создания процесса)FailedOL_E_START_SESSION с внутренним кодом; возможно OL_E_PROCESS_CLEANUP_FAILED, OL_E_RESTORE_DEFERRED, OL_E_RESTORE_FAILED.
Начальная загрузка плагинаFailedOL_E_PLUGIN_NOT_LOADED, OL_E_PLUGIN_PROTOCOL_MISMATCH, OL_E_CAPABILITY_UNAVAILABLE, OL_E_BUILD_VALIDATION_FAILED, OL_E_HEADLESS_ARM_FAILED.
Запуск мираFailedOL_E_WORLD_START_FAILED, OL_E_SITUATION_LOAD_FAILED, OL_E_STARTUP_TIMEOUT, OL_E_PROCESS_EXITED_EARLY.
РаботаFailed только при сбоях супервизораOL_E_PROCESS_SUPERVISION; ошибки runtime-операций никогда не переводят сеанс в состояние ошибки.
Завершение процесса и восстановлениеFailedOL_E_RESTORE_FAILED, OL_E_RESTORE_DEFERRED, OL_E_PROCESS_CLEANUP_FAILED.

Каждый путь ошибки всё равно пытается завершить процесс и выполнить восстановление; оставшийся журнал восстанавливается при следующем запуске или через RecoverPendingAsync / /recover (транзакции и восстановление после сбоя).