Индикатор в трее Windows

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

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

Каждый автономный сеанс владельца (запущенный через OmsiLaunch.exe или OmsiLaunchW.exe) показывает значок в области уведомлений, который отображает состояние сеанса и позволяет пользователю его завершить. На этой странице описан индикатор в том виде, как он реализован классами SessionTrayIndicator, StatusWindow и StopConfirmationWindow в tools\OmsiLaunch.Cli\WindowsHost.cs, презентером только для чтения SessionStatusPresenter (tools\OmsiLaunch.Cli\SessionStatusPresenter.cs) и реестром строк WindowsUiStrings (tools\OmsiLaunch.Cli\WindowsUiStrings.cs), а также диалоги ошибок OmsiLaunchW.exe из WindowsHost. Трей — это только адаптер представления: он не владеет ни OMSI, ни восстановлением после сбоя, а его действие остановки передаёт сигнал по тому же каноническому пути остановки через владельца, что и session stop (см. справочник CLI и локальное управление).

Когда существует значок#

ЭтапПоведение
СозданиеСразу после возврата из StartSessionAsync, до того как сеанс достигнет Running, если не задан SuppressTrayIcon. Поэтому значок существует в состояниях StartingProcess, WaitingForPlugin, StartingWorld и EnteringGameplay.
Бюджет запускаПоток UI (STA, фоновый, с именем OmsiLaunch tray) должен опубликовать значок в течение 2 s. В противном случае, а также при любом исключении во время создания, индикатор освобождается, и сеанс продолжается без значка; в лог записывается startup-timeout или исключение. Медленный запуск никогда не оставляет видимый «осиротевший» значок.
УдалениеВ блоке finally владельца, после того как сеанс завершился успешно или с ошибкой, и до CloseAsync. При освобождении в поток UI отправляется команда завершения (закрываются меню, диалог подтверждения и окно состояния, затем завершается цикл сообщений), поток ожидается до 2 s (при превышении в лог записывается dispose-timeout), NotifyIcon скрывается и освобождается.
SuppressTrayIconSessionPresentationSpec.SuppressTrayIcon (поле LaunchSpec, Presentation.SuppressTrayIcon, по умолчанию false). Задаётся через /spec и API; флага CLI нет. Интеграторы, которые отображают собственный элемент управления сеансом, устанавливают его в true; больше ничего в сеансе не меняется.
ХостыЗначок показывают и OmsiLaunch.exe (консоль), и OmsiLaunchW.exe (подсистема Windows); у сеансов OmsiLaunchW.exe, запущенных с /silent, другой видимой поверхности нет.

Значок и всплывающая подсказка#

  • Значок: значок, связанный с запущенным исполняемым файлом (Icon.ExtractAssociatedIcon(Application.ExecutablePath), встроенный значок OmsiLaunch); при неудаче используется SystemIcons.Application.
  • Текст всплывающей подсказки: Tray.Running (OmsiLaunch is running, то есть OmsiLaunch работает). Во время остановки текст не меняется (строки для состояния «остановка» пока нет; значок остаётся без изменений, пока владелец его не удалит).
  • Визуальные стили включены (Application.EnableVisualStyles).

Взаимодействие#

ActionРезультат
Щелчок правой кнопкойДелает окно трея окном переднего плана (это требуется для меню значков уведомлений; без этого меню может игнорировать щелчки и не закрываться, пока OMSI находится на переднем плане), затем открывает контекстное меню в реальной позиции курсора (Cursor.Position, а не координаты события, потому что NotifyIcon может сообщать (0,0) для событий, обрабатываемых оболочкой). Меню ограничивается рабочей областью экрана, на котором находится курсор.
Двойной щелчокОткрывает окно состояния (то же, что пункт меню Status).
Щелчок левой кнопкойНичего не происходит.
Пункт меню Status (Tray.Status)Открывает (или активирует, если оно уже открыто) окно состояния только для чтения.
Разделитель
Пункт меню End session (Tray.EndSession, описание для специальных возможностей Tray.EndSessionDescription)Открывает диалог подтверждения.

Окно состояния (снимок только для чтения)#

Открывается пунктом меню Status (состояние) или двойным щелчком по значку (SessionTrayIndicator.ShowStatus). Это диалог фиксированного размера, выровненный по центру, с автоматическим подбором размера, без кнопки на панели задач и с единственной кнопкой Close (Status.Close; закрыть его можно также клавишей Escape). Если выбрать Status, когда окно уже открыто, это окно активируется без повторного построения (оно сохраняет снимок момента первого открытия). Ошибка построения окна записывается в tray-host.log и не влияет на сеанс.

Это снимок состояния, а не живое представление. SessionStatusPresenter.Create(plan, status, ui) выполняется один раз при открытии окна: он читает разрешённый SessionPlan сеанса (спецификацию, которая фактически была спланирована) и один SessionStatus (только его State). Пока окно открыто, ничего не обновляется, и оно никогда не опрашивает OMSI (никаких runtime-операций, никаких значений телеметрии). Чтобы увидеть более новое состояние, закройте окно и откройте его снова.

Заголовок окна и заголовок содержимого: один и тот же текст — Status.SessionRunning (Session is running, то есть сеанс выполняется), если SessionStatus.State равно Running, в противном случае — необработанное имя SessionState (например, WaitingForPlugin, если окно открыто во время запуска, поскольку значок существует ещё до Running). Status.Title (OmsiLaunch session status) определён в таблице строк, но в этом выпуске не используется.

Разделы идут в указанном порядке; раздел не показывается, если в нём нет полей. Все значения берутся из спланированных LaunchSpec/SessionPlan и никогда из OMSI.

Раздел (английская метка)Поле (английская метка)Когда показываетсяЗначениеИсточник (публичный эквивалент)
SessionModeвсегдаNew session (WorldMode.NewMap), Saved situation (WorldMode.SavedSituation), Last map state (любой другой режим; никогда не достигается, потому что LastMapState неисполним)SessionPlan.Spec.World.Mode
SessionMapплан определил идентичность контента mapDisplayName карты (имя каталога карты, например Grundorf), иначе имя файла идентичности без расширениязапись SessionPlan.ResolvedContent с Kind = "map" (NEW_MAP); DiscoverAsync(Maps) даёт тот же DisplayName
SessionSituationSavedSituation с идентичностью ситуацииимя файла без расширения (situations\Linie 5.osn → Linie 5)Spec.World.SituationIdentity
SessionEntry pointбыла запрошена точка входаидентичность точки входа, если задана (в этом выпуске никогда не бывает исполнимой), иначе представленный индекс как целое число (1)Spec.World.EntrypointIdentity / PresentedEntrypointIndex
Session profileProfileиспользовался профиль сеанса (/predefined-profile)name профиляSpec.SessionProfile.Name (SessionProfileMetadata)
Session profilePresetкак выше, если у пресета есть имяname пресетаSpec.SessionProfile.PresetName
EnvironmentDate, Time, Weatherбыли запрошены явные/системные дата, время или погодаDD/MM/YYYY или System; HH:MM:SS или System; код ICAO, имя пресета или Real/currentSpec.Date, Spec.Time, Spec.EffectiveWeather
VehicleVehicle, Repaint, HOF, Fleet number, Registrationбыло запрошено поле транспортного средства игроказапрошенная идентичность/значениеSpec.PlayerVehicle
Configurationпо одному полю на каждый семантический параметрпараметр был задан (/set, settings профиля, LaunchSpec.Environment.*) и известен ConfigurationCatalogзапрошенное значение; для ключей, оканчивающихся на Percent, добавляется %, для ключей, оканчивающихся на DistanceMeters, — m. Метка — это ключ параметра, в котором каждая часть, разделённая точкой, пишется с заглавной буквы (graphics.maxFPS → Graphics MaxFPS)Spec.Environment.*; те же значения являются PlannedMutations плана
PresentationSplashвсегдаManaged или Original OMSISpec.EffectivePresentation.Splash
PresentationInternet texturesвсегдаOriginal OMSI (Native), Disabled, OverrideSpec.EffectiveInternetTextures.Mode

Разделы Environment и Vehicle никогда не могут появиться в выполняющемся сеансе Beta 3: запрос даты, времени, года, погоды или любого поля транспортного средства игрока делает план неисполнимым, поэтому такой сеанс не запускается (см. известные ограничения). Они предусмотрены для будущих build и покрыты офлайн-тестом презентера.

Подтверждение в runtime (UI на pt-BR, runtime-проверка T01): сеанс NEW_MAP на карте Grundorf показал Sessão em execução; Sessão: Modo: Nova sessão, Mapa: Grundorf, Ponto de entrada: 1; Apresentação: Splash: Gerenciado, Texturas da internet: OMSI original.

Те же данные доступны инструментам и без трея: session status через локальную плоскость управления даёт SessionId, State, Diagnostics и RuntimeEvents (в реальном времени), а вывод /plan --json владельца (или PlanSessionAsync) даёт спланированную спецификацию, разрешённый контент и запланированные изменения, которые окно показывает в сводном виде.

Завершение сеанса (с подтверждением)#

  1. StopConfirmationWindow: заголовок End session?, сообщение OMSI 2 will be closed and the OmsiLaunch managed session will end. (OMSI 2 будет закрыта, управляемый сеанс OmsiLaunch завершится), кнопки End session (по умолчанию, DialogResult.OK) и Cancel (Escape). Повторный запрос, пока диалог открыт, активирует его, а не открывает ещё один поверх.
  2. При OK трей вызывает requestCanonicalStop, который завершает сигнал controlStopped владельца; затем владелец вызывает StopAsync: OMSI принудительно завершается через TerminateProcess, и все файлы, которыми владеет сеанс, восстанавливаются. Сам трей никогда не завершает OMSI.
  3. Если запрос выбрасывает исключение, ошибка записывается в лог и показывается Stop.Failed (The session could not be ended. OMSI and its managed session remain active. — сеанс не удалось завершить, OMSI и её управляемый сеанс остаются активными).
  4. Трей не подтверждает успех; значок исчезает, когда владелец завершает восстановление и освобождает индикатор (runtime-проверка T01: владелец завершился через 607 ms после подтверждения End session).
  5. Cancel (или закрытие диалога) ничего не делает: сеанс продолжает выполняться (T01).
  6. Остановка, пришедшая из другого источника (session stop, Ctrl+C, /observe-seconds, завершение OMSI), когда открыто окно состояния или диалог подтверждения, закрывает их в ходе освобождения индикатора; владелец не ждёт пользователя (T02: владелец завершился через 725 ms после остановки через pipe при обоих открытых окнах).

Перезапуск Проводника (Explorer)#

TrayWindow — скрытое нативное окно, которое регистрирует оконное сообщение TaskbarCreated. При перезапуске Проводника (Explorer, оболочки) он рассылает это сообщение, и индикатор снова добавляет значок (Visible = false; Visible = true). Runtime-проверка T01: после того как explorer.exe был завершён и перезапущен Windows, значок снова появился в Shell_TrayWnd, а меню и окно состояния продолжили работать.

Локализация#

WindowsUiStrings.Resolve следует культуре интерфейса Windows (CultureInfo.CurrentUICulture), а не языку контента OMSI и не языку профиля сеанса. Порядок разрешения: точное имя культуры, затем двухбуквенный код языка, затем английский. Каждый ключ, для которого нет перевода, берётся из английского.

Ключи культурыЯзык
en, en-US, en-GBАнглийский (по умолчанию и как запасной вариант)
pt-BRБразильский португальский. pt-PT (и просто pt) намеренно использует английский.
de, de-DEНемецкий
fr, fr-FRФранцузский
pl, pl-PLПольский

Локализованные строки охватывают всплывающую подсказку, два пункта меню, окно состояния (заголовок, названия разделов, метки полей, Close), значения режима и представления, а также диалог подтверждения. Глоссарий ведётся в docs\windows-ui-localization.md; офлайн-тест windows-ui.localization-and-status (tests\OmsiLaunch.WindowsUiTests) проверяет разрешение строк и презентер.

Диалоги ошибок OmsiLaunchW.exe#

Когда задано OMSILAUNCH_WINDOWS_HOST=1 (его устанавливает OmsiLaunchW.exe), WindowsHost.ShowFailure заменяет вывод ошибок в консоль модальным окном сообщения (message box) с заголовком OmsiLaunch (значок ошибки): <message>, пустая строка, Code: OL_E_..., пустая строка, See .omsilaunch\diagnostics for details. Оно показывается при каждом вызове CliInput.WriteError (ошибки аргументов, OL_E_NO_ACTIVE_SESSION, OL_E_SESSION_ALREADY_ACTIVE, классифицированные исключения), когда план запуска неисполним (запасной текст The session plan is not runnable.; аудит документации BUG-06), и когда сеансу не удаётся достичь Running (The OMSI session did not reach gameplay. с последним диагностическим сообщением OL_E_ или OL_E_SESSION_START_FAILED, если такого нет). Полное описание поведения OmsiLaunchW.exe приведено в справочнике по OmsiLaunchW.exe. Под OmsiLaunch.exe та же функция ничего не делает. Ошибку запуска хоста .NET (коды shim 100..106) показывает сам нативный shim; см. коды выхода.

Расположение лога#

<root>\.omsilaunch\diagnostics\tray-host.log, одна строка на запись: метка времени UTC в формате ISO-8601, символ табуляции, затем запись. Записи: created, removed, startup-timeout, startup-cancelled (освобождение опередило запуск, и цикл был пропущен), dispose-timeout, а также полные тексты исключений при сбоях UI. Логирование выполняется по принципу best effort и никогда не выбрасывает исключений. Логи хоста сеанса (<sessionId>-host.log) записываются владельцем в тот же каталог; лог трея не имеет префикса сеанса и не удаляется при хранении последних 50 сеансов.

Гарантии жизненного цикла#

  • Трей никогда не владеет сеансом: он не может запустить OMSI, не может восстанавливать файлы и не может обойти путь остановки через владельца.
  • Каждый путь завершения владельца (нормальное завершение, завершение OMSI, Ctrl+C, закрытие консоли, остановка через pipe, исключение, /observe-seconds) освобождает индикатор до CloseAsync, поэтому ни один значок не переживает свой сеанс, кроме случая, когда процесс владельца принудительно уничтожен (Windows удаляет осиротевшие значки при следующем наведении мыши).
  • Создание и освобождение сериализуются под блокировкой: если освобождение выигрывает гонку, поток UI пропускает свой цикл сообщений и сразу выполняет очистку.
  • Вся работа Windows Forms выполняется в выделенном потоке STA; сторонние потоки только отправляют в него задания через скрытый элемент управления для маршалинга.

Остаточные оговорки (из комментариев в коде)#

  • Текста всплывающей подсказки для состояния «остановка» нет; значок показывает OmsiLaunch is running, пока его не удалят.
  • NotifyIcon может сообщать координаты мыши (0,0) для событий, обрабатываемых оболочкой; вместо них читается позиция курсора.
  • Сообщения трея продолжают поступать, пока диалог подтверждения открыт в модальном режиме; второй диалог подтверждения поверх первого не открывается.
  • Если поток UI не завершается в пределах бюджета освобождения 2 s, владелец продолжает работу без ожидания (dispose-timeout).
  • Окно состояния — это снимок спланированных значений на момент открытия; оно не обновляется и никогда не читает OMSI.

Подтверждения в runtime#

Наблюдалось в реальных сеансах на авторизованной установке в раунде runtime-проверки (research/reports/runtime-closure/FINAL-RUNTIME-VALIDATION-REPORT.md, интерфейс Windows на pt-BR; см. статус runtime-проверки):

  • Значок регистрируется в реальной области уведомлений (Shell_TrayWnd) под OmsiLaunchW.exe и удаляется после восстановления (T01..T04).
  • «End session» с подтверждением запускает каноническую остановку и точное восстановление; Cancel оставляет сеанс выполняющимся (T01, T03).
  • Значок создаётся заново после перезапуска Проводника (Explorer) (TaskbarCreated, T01).
  • Окна состояния и подтверждения закрываются владельцем, если остановка приходит, пока они открыты (T02).
  • Диалоги ошибок OmsiLaunchW.exe для ошибки аргумента (OL_E_INVALID_ARGUMENT), отсутствия активного сеанса (OL_E_NO_ACTIVE_SESSION) и сеанса, завершившегося ошибкой до начала игрового процесса (OL_E_WORLD_START_FAILED) (T04). В последнем случае диалог показывает в качестве сообщения данные об ошибке, переданные плагином.
  • /silent отсоединяется: лаунчер возвращает управление, а хост Windows продолжает вести сеанс (T04).
  • Бюджет ProcessExit 4 s при закрытии консоли (L04, владелец в консоли).

Не воспроизведено: диалоги кодов выхода shim загрузчика (100..106).