Индикатор в трее Windows
Перевод исходной страницы на английском языке для 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 скрывается и освобождается. |
SuppressTrayIcon | SessionPresentationSpec.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.
| Раздел (английская метка) | Поле (английская метка) | Когда показывается | Значение | Источник (публичный эквивалент) |
|---|---|---|---|---|
Session | Mode | всегда | New session (WorldMode.NewMap), Saved situation (WorldMode.SavedSituation), Last map state (любой другой режим; никогда не достигается, потому что LastMapState неисполним) | SessionPlan.Spec.World.Mode |
Session | Map | план определил идентичность контента map | DisplayName карты (имя каталога карты, например Grundorf), иначе имя файла идентичности без расширения | запись SessionPlan.ResolvedContent с Kind = "map" (NEW_MAP); DiscoverAsync(Maps) даёт тот же DisplayName |
Session | Situation | SavedSituation с идентичностью ситуации | имя файла без расширения (situations\Linie 5.osn → Linie 5) | Spec.World.SituationIdentity |
Session | Entry point | была запрошена точка входа | идентичность точки входа, если задана (в этом выпуске никогда не бывает исполнимой), иначе представленный индекс как целое число (1) | Spec.World.EntrypointIdentity / PresentedEntrypointIndex |
Session profile | Profile | использовался профиль сеанса (/predefined-profile) | name профиля | Spec.SessionProfile.Name (SessionProfileMetadata) |
Session profile | Preset | как выше, если у пресета есть имя | name пресета | Spec.SessionProfile.PresetName |
Environment | Date, Time, Weather | были запрошены явные/системные дата, время или погода | DD/MM/YYYY или System; HH:MM:SS или System; код ICAO, имя пресета или Real/current | Spec.Date, Spec.Time, Spec.EffectiveWeather |
Vehicle | Vehicle, Repaint, HOF, Fleet number, Registration | было запрошено поле транспортного средства игрока | запрошенная идентичность/значение | Spec.PlayerVehicle |
Configuration | по одному полю на каждый семантический параметр | параметр был задан (/set, settings профиля, LaunchSpec.Environment.*) и известен ConfigurationCatalog | запрошенное значение; для ключей, оканчивающихся на Percent, добавляется %, для ключей, оканчивающихся на DistanceMeters, — m. Метка — это ключ параметра, в котором каждая часть, разделённая точкой, пишется с заглавной буквы (graphics.maxFPS → Graphics MaxFPS) | Spec.Environment.*; те же значения являются PlannedMutations плана |
Presentation | Splash | всегда | Managed или Original OMSI | Spec.EffectivePresentation.Splash |
Presentation | Internet textures | всегда | Original OMSI (Native), Disabled, Override | Spec.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) даёт спланированную спецификацию, разрешённый контент и запланированные изменения, которые окно показывает в сводном виде.
Завершение сеанса (с подтверждением)#
StopConfirmationWindow: заголовокEnd session?, сообщениеOMSI 2 will be closed and the OmsiLaunch managed session will end.(OMSI 2 будет закрыта, управляемый сеанс OmsiLaunch завершится), кнопкиEnd session(по умолчанию,DialogResult.OK) иCancel(Escape). Повторный запрос, пока диалог открыт, активирует его, а не открывает ещё один поверх.- При
OKтрей вызываетrequestCanonicalStop, который завершает сигналcontrolStoppedвладельца; затем владелец вызываетStopAsync: OMSI принудительно завершается черезTerminateProcess, и все файлы, которыми владеет сеанс, восстанавливаются. Сам трей никогда не завершает OMSI. - Если запрос выбрасывает исключение, ошибка записывается в лог и показывается
Stop.Failed(The session could not be ended. OMSI and its managed session remain active.— сеанс не удалось завершить, OMSI и её управляемый сеанс остаются активными). - Трей не подтверждает успех; значок исчезает, когда владелец завершает восстановление и освобождает индикатор (runtime-проверка
T01: владелец завершился через 607 ms после подтвержденияEnd session). Cancel(или закрытие диалога) ничего не делает: сеанс продолжает выполняться (T01).- Остановка, пришедшая из другого источника (
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).- Бюджет
ProcessExit4 s при закрытии консоли (L04, владелец в консоли).
Не воспроизведено: диалоги кодов выхода shim загрузчика (100..106).