Как понимать изоляцию сеансов Windows — Session 0, RDP и одновременная работа нескольких пользователей

· · Session 0, Изоляция сеансов, RDP, Удалённый рабочий стол, Службы Windows, Многопользовательский режим, Windows, .NET, C#, Архитектура, Технический консалтинг

«Диалоговое окно, показанное службой, никто не видит». «Как только кто-то подключился по RDP, экран физической машины переключился на экран входа». «Защита единственного экземпляра должна была предотвращать повторный запуск, но вход под другим пользователем спокойно запустил вторую копию». Консультации по бизнес-приложениям снова и снова сталкиваются с такими симптомами, и за всеми ними стоит один и тот же пробел: неполное понимание понятия «сеанс» (session) в Windows. Сеансы затрагивают и проектирование служб, и эксплуатацию удалённого рабочего стола, и защиту единственного экземпляра для именованных объектов, однако редко можно встретить систематическое объяснение этой темы в одном месте.

В этой статье с практической точки зрения разбирается, почему была введена изоляция Session 0, как ведут себя сеансы при подключении по RDP, как именованные объекты изолируются по сеансам, а также типичные ошибки проектирования, встречающиеся в средах с общими ПК и RDS (Remote Desktop Services).

1. Сначала — суть

  • Службы выполняются в выделенном сеансе под названием Session 0, изолированном от интерактивного сеанса любого вошедшего в систему пользователя. Служба не может напрямую показать диалоговое окно, а если попытается — пользователь его всё равно не увидит.1
  • Официально рекомендованное решение для службы, которой нужен интерфейс, — вынести UI в отдельный процесс (работающий в сеансе пользователя) и общаться с ним через IPC. Запуск программы с интерфейсом от имени интерактивного пользователя через Планировщик заданий — ещё один доступный вариант.2
  • Windows Server (RDS) построен вокруг нескольких одновременных сеансов, а клиентские ОС вроде Windows 10/11 Pro принципиально однопользовательские. За исключением специального издания «Enterprise multi-session», созданного для Azure Virtual Desktop, обычные установки клиентской ОС не рассчитаны на несколько одновременных интерактивных сеансов.3
  • Именованные объекты ядра (мьютексы, события, семафоры и т. д.) имеют собственное пространство имён для каждого сеанса. Если нужно свести объект к единственному экземпляру на всю машину и на все сеансы, имя нужно явно снабдить префиксом Global\ — иначе защита единственного экземпляра будет вести себя не так, как ожидается. Именованные каналы (named pipes) находятся вне этого механизма: по умолчанию они используют отдельное пространство имён, доступное в масштабе всей машины (а также удалённо, если запущена служба сервера), поэтому, если нужно различать по сеансу или по пользователю, нужно встроить в само имя канала что-то вроде идентификатора сеанса.45
  • Тип текущего сеанса можно определить через переменную окружения SESSIONNAME или GetSystemMetrics(SM_REMOTESESSION). Однако бывают случаи — например, при использовании RemoteFX vGPU — когда определение отклоняется от ожидаемого поведения, поэтому не стоит полагаться на это чрезмерно.67
  • Вопрос о том, должно ли поведение на общих ПК/в RDS входить в требования, нужно уточнять в самом начале этапа проектирования. Коллизии путей временных файлов, путаница с AppData и одновременный доступ к устройствам/донглам — классические аварии, которые взрываются в тот момент, когда приложение, созданное для одного отдельного ПК, разворачивают в RDS (см. главу 7).

2. Что такое изоляция Session 0 — и почему произошло разделение с пользовательскими сеансами

До Windows Vista / Windows Server 2008 служба и первый вошедший в систему пользователь делили один и тот же Session 0. Процессы в рамках одного сеанса могут обмениваться оконными сообщениями независимо от уровня привилегий друг друга. Процесс со стандартными привилегиями мог отправить специально сформированное сообщение окну службы, работающей от имени SYSTEM, перехватить обработку сообщений службы и повысить свои привилегии — весь этот класс атак с повышением привилегий, эксплуатирующих недостатки обработки оконных сообщений, неоднократно фиксировался на протяжении 2000-х годов, и на деле несколько уязвимостей повышения привилегий, эксплуатирующих недостатки обработки оконных сообщений в ядре Windows (win32k.sys), были устранены за прошедшие годы.8

Эта картина изменилась начиная с Windows Vista. Session 0 стал предназначаться исключительно для служб и приложений, не привязанных к интерактивному пользовательскому сеансу, а первому вошедшему в систему пользователю теперь назначается Session 1, следующему — Session 2 и так далее — каждому в своём сеансе, отдельном от службы. Session 0 не поддерживает интерактивную работу, и службы не могут отправлять сообщения приложениям, а приложения — службам. Служба также не может напрямую отобразить элементы UI, такие как диалоговое окно. Благодаря этому изменению сам путь, позволявший процессу со стандартными привилегиями отправлять сообщения напрямую в окно службы, был структурно закрыт.1

Иными словами, изоляция Session 0 — это не просто эксплуатационное ограничение вроде «службы работают в фоне, поэтому у них нет экрана», а решение по безопасности, которое структурно блокирует обмен оконными сообщениями через границу привилегий. Единственный оставшийся способ для службы хоть как-то показать интерфейс — функция WTSSendMessage, API, выводящий простое окно сообщения в другом сеансе, но его нельзя использовать для сложных диалогов или двустороннего взаимодействия.1

3. Службы не могут показывать интерфейс — практические ограничения и обходные схемы

Практическое следствие изоляции Session 0 простое. Если служба вызывает MessageBox.Show или пытается показать форму, на экране вошедшего в систему пользователя ничего не появляется. Хуже того, это классическая причина зависания всего процесса в бесконечном ожидании нажатия кнопки OK, которую никто никогда не сможет нажать. При переносе старого кода (чего-то, что когда-то работало как «интерактивная служба» ещё во времена Windows XP) в службу всегда проверяйте, не остались ли в коде вызовы отображения интерфейса.

Есть два официально задокументированных решения.2

  • Вынести UI в отдельный процесс (работающий в сеансе интерактивного пользователя) и общаться через IPC. Служба выполняет работу и передаёт результаты или запросы ввода процессу UI. Процесс UI показывает их пользователю в виде значка в области уведомлений или диалогового окна и возвращает службе результат операции. При использовании IPC, например именованных каналов, нужно спроектировать списки контроля доступа (ACL) так, чтобы ничего не оказалось непреднамеренно доступно по сети. В средах, где несколько пользователей работают одновременно, рекомендуется различать сеансы, например встраивая ID сеанса в имя канала.2 О выборе механизма IPC см. сопутствующую статью, опубликованную в тот же день: «Таблица решений для межпроцессного взаимодействия в Windows». О том, как строить саму службу, а также об эксплуатационных аспектах проектирования — типе запуска, учётной записи выполнения, параметрах восстановления — рассказано в статье «Как построить службу Windows».
  • Запускать программу от имени интерактивного пользователя через Планировщик заданий. Вместо того чтобы держать программу постоянно работающей как службу, эта конфигурация запускает программу с интерфейсом под собственными привилегиями и в сеансе вошедшего в систему пользователя. Здесь важно учесть один момент: «задача, зарегистрированная под SYSTEM», ни в коем случае не должна быть той, что показывает интерфейс. У учётной записи SYSTEM нет прав интерактивного входа, и пользователь не может видеть программу или задачу, работающую с привилегиями SYSTEM, и взаимодействовать с ней.9 Модель повышенной задачи (elevated task) предполагает, что задача SYSTEM — дескриптор безопасности которой позволяет запускать её обычному пользователю — берёт на себя только те операции, которые требуют прав администратора, а сам интерфейс работает в отдельной программе под собственными привилегиями и в сеансе вошедшего в систему пользователя (либо как та же операция, зарегистрированная в виде задачи под учётной записью пользователя); в этом и заключается всё разделение ролей.10 Держите задачу SYSTEM строго в роли «безголового брокера» и всегда позволяйте видимому интерфейсу работать в процессе, выполняющемся в собственном сеансе пользователя.

Каким бы ни был выбранный подход, суть в том, чтобы с первого дня заложить в проектирование принцип «служба остаётся за кулисами, интерфейс живёт в отдельном процессе». На практике мы регулярно наблюдаем, что попытка добавить это позже упирается в код внутри службы, уже глубоко завязанный на интерфейс, что резко повышает стоимость их разделения.

4. Как ведут себя сеансы при подключении по RDP — разница между RDS и клиентской ОС

Путаница вида «на сервере и на клиенте один и тот же RDP, почему же поведение отличается?» связана с наличием или отсутствием роли Remote Desktop Services (RDS).

Windows Server с ролью RDS спроектирован вокруг нескольких пользователей, одновременно входящих на один сервер, каждый из которых работает в независимом сеансе со своим рабочим столом и приложениями — конфигурация, изначально построенная на нескольких одновременных сеансах.11 В RDS обычные административные RDP-подключения (для администраторов) поддерживают до двух сеансов без необходимости в CAL, но превышение этого числа или одновременное подключение обычных пользователей требует развёртывания роли RD Session Host вместе с надлежащими лицензиями RDS CAL (Client Access License).12 CAL бывают двух моделей — на устройство и на пользователя, и детали лицензирования нужно уточнять для каждой среды отдельно.13

С другой стороны, обычная клиентская ОС, такая как Windows 10/11 Pro или Enterprise, не несёт роли RDS и, в принципе, является однопользовательской (single-session). Существует специальное издание клиентской ОС под названием «Windows Enterprise multi-session», способное удерживать несколько интерактивных сеансов одновременно, но оно предлагается исключительно для Azure Virtual Desktop — обычные, распространяемые локально установки клиентской ОС не рассчитаны на поддержку нескольких одновременных пользовательских сеансов.3 На практике, в соответствии со знакомым опытом «я подключился по RDP к ПК коллеги, и экран на его рабочем месте переключился на экран блокировки», безопаснее исходить из того, что на клиентских ОС сеанс физической консоли (кто-то, локально управляющий машиной) и удалённое подключение в основном не могут сосуществовать одновременно.

Вот сводка различий, важных для проектирования бизнес-приложений.

Аспект Windows Server + RDS Клиентская ОС (Windows 10/11 Pro и т. п.)
Одновременные сеансы Несколько пользователей могут входить одновременно11 Принципиально однопользовательский режим. Несколько интерактивных сеансов не поддерживаются, за исключением издания multi-session, доступного только для AVD3
Лицензирование Требуется RD Session Host + лицензии RDS CAL (сверх двух административных подключений)12 Сама функция удалённого рабочего стола поставляется вместе с ОС, но требования к лицензированию и допустимые издания нужно уточнять отдельно
Типичное применение Работа бизнес-приложений на общих терминалах, среды тонких клиентов Удалённое обслуживание личного ПК, доступ при удалённой работе

Если не учесть это различие — предполагая, что «раз это работало на клиентской ОС на моей машине разработки, значит, так же будет работать и в среде RDS общего сервера», — можно упустить из виду ошибки, специфичные для многопользовательской конкуренции, рассмотренные в главе 7. И наоборот, если предполагается только однопользовательская работа на клиентской ОС, нет необходимости заранее нести затраты на проектирование поддержки общего ПК. Это решение нужно принимать на раннем этапе определения требований (глава 8).

5. Определение текущего сеанса

Есть немало ситуаций, когда нужно знать, в каком сеансе сейчас работает ваш процесс — например, чтобы отключить тяжёлый эффект отрисовки во время фоновой обработки или отключить функцию, требовательную к пропускной способности, в удалённом сеансе. Определить это можно несколькими способами.

  • Переменная окружения SESSIONNAME: устанавливается в Console для консольного сеанса или в имя, начинающееся с RDP-Tcp#, для RDP-подключения. Это та же информация, что показана в столбце SESSIONNAME, выводимом командой qwinsta.14 Пригодна для быстрой проверки, но, поскольку это не официальный API, лучше не полагаться на неё слишком сильно.
  • GetSystemMetrics(SM_REMOTESESSION): Win32 API, который сообщает, находитесь ли вы в удалённом сеансе. Возвращает ненулевое значение, если вызывающий процесс связан с клиентским сеансом Terminal Services. Однако есть оговорка: начиная с Windows 8/Server 2012, в удалённых сеансах с RemoteFX vGPU эта функция ошибочно определяет удалённый сеанс как локальный. В этом случае задокументированный обходной путь — сравнить значение GlassSessionId из реестра с текущим ID сеанса.67
  • SystemParameters.IsRemoteSession в .NET (WPF): предоставляет ту же проверку, что выполняет GetSystemMetrics(SM_REMOTESESSION), в виде свойства, доступного из WPF-приложения.15 Из WinForms или консольного приложения её пришлось бы вызывать напрямую через P/Invoke.
  • WTSGetActiveConsoleSessionId: API, получающий ID сеанса, подключённого к физической консоли. Сравнение с собственным ID сеанса позволяет определить: «работаю ли я на физической консоли?». Обратите внимание, что когда к физической консоли никто не подключён (то есть во время перехода), возвращается 0xFFFFFFFF.16

Не используйте WTSGetActiveConsoleSessionId для определения сеансов в среде RDS. Как следует из названия, эта функция возвращает только «сеанс, подключённый к физической консоли» — пользователь, вошедший через RDP, ею не охватывается.16 Если службе нужно узнать, в каком сеансе показать сопутствующий UI, этого API достаточно, когда ожидается только пользователь, подключённый к консоли, но в среде RDS, где одновременно могут быть подключены несколько RDP-пользователей, следует вместо этого перечислять работающие сеансы через WTSEnumerateSessions17 либо просто использовать ID сеанса, переданный уведомлением об изменении сеанса (WM_WTSSESSION_CHANGE). Эта путаница — классическая, трудно обнаруживаемая причина ошибок, при которых уведомление UI просто не появляется — или появляется не в том сеансе — именно для пользователей, подключённых по RDP. Твёрдо держите это различие в уме, решая, в каком сеансе запускать процесс UI для конфигурации IPC, описанной в главе 3.

6. Изоляция именованных объектов по сеансам

Именованные объекты ядра — мьютексы, события, семафоры, ожидаемые таймеры, объекты отображения файлов — имеют независимое пространство имён для каждого сеанса. Даже если создать в одном сеансе мьютекс с именем MyAppMutex, попытка открыть объект с тем же именем из другого сеанса создаст совершенно новый, отдельный объект. Для процессов, которые в основном работают в Session 0, таких как службы, или для конфигураций клиент/сервер, желающих делить объект между несколькими сеансами, его можно явно поместить в глобальное пространство имён, снабдив имя префиксом Global\. И наоборот, префикс Local\ явно помещает его в пространство имён вашего собственного сеанса.4

Именно здесь это действительно важно на практике: мьютекс, используемый для защиты единственного экземпляра.

  • Если всё, что нужно, — это «не дать одному и тому же пользователю запустить две копии приложения в рамках одного сеанса», достаточно мьютекса с именем по умолчанию (без префикса). Пространство имён по сеансам включается автоматически, поэтому сеанс другого пользователя воспринимается как другой мьютекс.
  • Если нужно «даже при одновременном входе нескольких пользователей в среде RDS ограничить приложение единственным экземпляром на всю машину», достичь этой цели можно только явно используя Global\. Оставленное по умолчанию, оно в итоге позволяет каждому пользователю запускать собственную копию — ошибка, которая проявляется как «защита единственного экземпляра должна была работать, но всё равно запустилось несколько экземпляров».

Обратите внимание также, что процессу вне Session 0 требуется привилегия SeCreateGlobalPrivilege, чтобы создать новый объект отображения файла или объект символьной ссылки в глобальном пространстве имён (для простого открытия существующего объекта она не требуется). Эта проверка привилегии применяется только к отображениям файлов и символьным ссылкам — она не накладывается на создание мьютексов или событий, — но её стоит помнить для любой конструкции, использующей разделяемую память (файл, отображаемый в памяти) в глобальном пространстве имён.4

Есть и общий принцип для клиент-серверных приложений: рекомендация — не приравнивать «подключение с одной машины» к «одному пользовательскому сеансу». С учётом того, что с одной машины одновременно могут быть установлены несколько сеансов (RDP-подключения нескольких пользователей), серверной стороне рекомендуется идентифицировать сеансы, например, с помощью ProcessIdToSessionId, и выделять отдельный канал связи для каждого сеанса.18

7. Типичные ошибки проектирования на общих ПК и в средах RDS

Есть несколько повторяющихся шаблонов сбоев, которые взрываются в тот момент, когда приложение, созданное с расчётом только на одиночный ПК и работу только с консолью, разворачивают на общем ПК или в среде RDS.

  • Коллизии путей временных файлов. По умолчанию RDS создаёт отдельную временную папку для каждого сеанса (внутри профиля пользователя, с ID сеанса, встроенным в имя).19 Иными словами, если приложение просто использует переменную окружения %TEMP%, оно бесплатно получает выгоду от этой изоляции. Проблемы начинаются, когда приложение жёстко прописывает фиксированный путь, например C:\Work\temp, вместо использования %TEMP%. Если несколько сеансов одной и той же учётной записи или несколько пользователей одновременно пишут по одному и тому же фиксированному пути, они, конечно же, будут конкурировать за файл.
  • Путаница с AppData. Куда сохранять пользовательские настройки, кэши и журналы, нужно решать, хорошо понимая механизм пользовательских профилей Windows — локальный или роуминговый, а также когда использовать %LOCALAPPDATA%, а когда %APPDATA%. На общем ПК небрежное проектирование в этой области склонно проявляться как «мой экран изменился из-за чужих настроек» или «журнал был перезаписан». Подробности см. в статье «Введение в пользовательские профили Windows».
  • Предположение об одновременном доступе к устройствам, донглам и последовательным портам. Доступ к локальному устройству на стороне клиента или последовательному порту из сеанса RDS проходит через механизм перенаправления. Если попытаться дать нескольким сеансам одновременно использовать приложение, зависящее от физического USB-донгла или устройства последовательной связи, возникнут проблемы: само оборудование может изначально не поддерживать одновременный доступ, либо видимость зависит от настройки перенаправления и может присутствовать или отсутствовать. То, как устройство, подключённое на стороне клиента, видно из сеанса на стороне сервера, зависит от конфигурации перенаправления, поэтому это необходимо подтвердить на этапе проектирования.20
  • Жёстко заданные принтеры и диски. Жёсткое прописывание имени принтера по умолчанию или буквы диска внутри приложения легко ломается в среде общего ПК, где перенаправленный принтер по умолчанию или подключённый диск различаются для каждого пользователя. Безопаснее получать имя принтера во время выполнения и, где возможно, работать с дисками через UNC-пути.
  • Неверное чтение HKCU / пользовательского реестра. Стоит также отметить, что HKEY_CURRENT_USER привязан к вошедшему в систему пользователю, а не к сеансу. В конфигурации, где у одного и того же пользователя несколько сеансов (например, при совместном использовании одной учётной записи в RDS), значения HKCU являются общими для всех сеансов этого пользователя — поэтому хранение в HKCU состояния времени выполнения, которое на самом деле должно быть изолировано по сеансу, непреднамеренно просочится в другие сеансы. Для информации, требующей изоляции по сеансу, следует вместо этого использовать пространства имён по сеансам из главы 6 или путь файла, включающий ID сеанса.

Все эти случаи относятся к тому типу ошибок, которые «никогда не заметишь на одном отдельном ПК» и которые «проявляются только тогда, когда приложение реально одновременно используют несколько пользователей», поэтому типичный сценарий таков: тестовая среда, состоящая только из одной автономной машины разработки, никогда не воспроизводит проблему, и она впервые появляется на общем ПК или в среде RDS в production.

8. Таблица решений — должна ли поддержка «общего ПК/RDS» входить в требования?

Вопрос о том, входит ли поведение на общем ПК/в RDS в ваши требования, нужно уточнить при определении требований, до начала реализации. Если вы решили включить это, используйте следующие пункты как контрольный список.

Аспект Допустимо предполагать только отдельный ПК/консоль Требуется поддержка общего ПК/RDS (одновременно несколько пользователей)
Временные файлы / настройки Фиксированный путь причиняет мало реального вреда Обязательны пути на пользователя/на сеанс, такие как %TEMP% / %LOCALAPPDATA% (глава 7)
Именованные объекты Допустимо оставить по умолчанию (пространство имён сеанса) Если нужен «один экземпляр на всю машину», явно используйте Global\. Если достаточно «одного на сеанс», оставьте значение по умолчанию как есть. Если нужен «один на пользователя, охватывающий все его сеансы», объедините Global\ с SID пользователя (глава 6; см. также статью о защите единственного экземпляра)
Устройства / донглы Простое прямое локальное подключение Проектировать с учётом того, что доступ идёт через перенаправление, и одновременный доступ может быть невозможен
UI и службы Легко удержать всё на одном рабочем столе Разделить на отдельный процесс + IPC с учётом изоляции Session 0 (глава 3)
Лицензирование / биллинг Просто считать по числу машин Уточнить концепцию количества одновременных сеансов и необходимость лицензий RDS CAL при определении требований13

Ось решения проста: договоритесь с заказчиком на раннем этапе разработки, должно ли «это приложение просто автономно работать на ПК одного человека» или «его могут одновременно использовать несколько людей на общем терминале или в среде RDS». Оставьте этот момент неопределённым, пока идёт реализация, — и в итоге придётся полностью переделывать каждый из перечисленных выше пунктов.

9. Пример реализации — грамотное использование определения сеанса и пространств имён

Вот как методы определения из глав 5 и 6, а также выбор именования мьютекса, объединяются в коде .NET.

using System.Runtime.InteropServices;

internal static class SessionInfo
{
    [DllImport("user32.dll")]
    private static extern int GetSystemMetrics(int nIndex);

    private const int SM_REMOTESESSION = 0x1000;

    // Для WPF-приложения System.Windows.SystemParameters.IsRemoteSession возвращает тот же результат
    public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;

    // Быстрая, приблизительная проверка. Это не официальный API, поэтому используйте
    // её только для журналирования/диагностики, а не для важных ветвлений
    public static string? SessionName() =>
        Environment.GetEnvironmentVariable("SESSIONNAME");
}

Мьютекс, используемый для защиты единственного экземпляра, должен различать три случая: «один на всю машину», «один на сеанс» и «один на пользователя, охватывающий все его сеансы». Третий случай (на пользователя, через сеансы) невозможно реализовать одним лишь Global\ по умолчанию — имя также должно включать SID пользователя. Это подробно рассмотрено в статье «Предотвращение повторного запуска в приложениях Windows».

// Требуется: using System.Security.AccessControl; и using System.Security.Principal;
// (также требуется NuGet-пакет System.Threading.AccessControl)

// Замысел: даже при одновременном входе нескольких пользователей на RDS/общем ПК
// сохранять ровно один экземпляр на всю машину.
// Mutex/MutexAcl.Create в .NET внутренне запрашивает SYNCHRONIZE и MUTEX_MODIFY_STATE,
// а также DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER, даже просто чтобы открыть
// существующий мьютекс. Это означает, что если «Прошедшим проверку пользователям»
// не предоставлен эквивалент FullControl, легитимный пользователь, отличный от
// создателя, получит UnauthorizedAccessException в момент проверки createdNew.
// (Компромисс: это также оставляет ACL доступным для последующего изменения любым
// прошедшим проверку пользователем. Если это неприемлемо, откажитесь от класса
// Mutex и вызывайте CreateMutexEx напрямую через P/Invoke, запрашивая только
// минимально необходимые права доступа)
//
// Осторожно: этот ACL применяется только в том случае, если именно вы первым
// создали мьютекс. Global\ мьютекс с фиксированным именем рискует быть заранее
// создан другим пользователем (захват имени), и в этом случае createdGlobal
// вернёт false, либо вы получите UnauthorizedAccessException в зависимости от
// ACL той стороны. Если вам действительно нужно исключить остальных, сочетайте
// труднопредсказуемое имя с другим механизмом исключения (подробности см. в
// главе 5 сопутствующей статьи)
var globalMutexSecurity = new MutexSecurity();
globalMutexSecurity.AddAccessRule(new MutexAccessRule(
    new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, domainSid: null),
    MutexRights.FullControl,
    AccessControlType.Allow));

using var globalMutex = MutexAcl.Create(
    initiallyOwned: false,
    name: @"Global\KomuraSoft.MyApp.SingleInstance",
    createdNew: out bool createdGlobal,
    mutexSecurity: globalMutexSecurity);

// Замысел: достаточно одного экземпляра на сеанс (несколько сеансов одного и того же
// пользователя вполне могут рассматриваться отдельно друг от друга).
// Оставить в пространстве имён сеанса по умолчанию как есть
using var perSessionMutex = new Mutex(
    initiallyOwned: false,
    name: "KomuraSoft.MyApp.SingleInstance",
    createdNew: out bool createdPerSession);

if (!createdGlobal)
{
    // Уже работает где-то ещё на машине, включая другие сеансы
    return;
}

Создание мьютекса с префиксом Global\ само по себе не требует специальных привилегий (как упоминалось ранее, проверка привилегии применяется только к созданию новых объектов отображения файлов и символьных ссылок).4 Оставлять мьютекс в пространстве имён сеанса по умолчанию — ошибка, если ваш замысел — «один на пользователя». Нередко один и тот же пользователь одновременно удерживает отключённый RDP-сеанс и новый консольный/RDP-сеанс, и в этом случае каждый сеанс рассматривается как отдельный мьютекс — поэтому, оставив значение по умолчанию как есть, тот же пользователь может в итоге запустить второй экземпляр. Какой из вариантов — «один на всю машину», «один на сеанс» или «один на пользователя через сеансы» — является верным требованием, зависит от природы приложения, поэтому уточните требование прежде, чем остановиться на схеме именования.

10. Заключение

«Сеанс» Windows — это понятие, которое постоянно всплывает в темах, на первый взгляд не связанных друг с другом, — проектировании служб, эксплуатации удалённого рабочего стола, защите единственного экземпляра именованных объектов. Основная структура сводится к трём пунктам. Служба работает в Session 0, специальном изолированном сеансе, и не может напрямую показывать интерфейс. RDS построен вокруг нескольких одновременных сеансов, тогда как обычная клиентская ОС принципиально однопользовательская. И именованные объекты имеют пространство имён по сеансам, из-за чего нужно решать, исходя из замысла, требуется ли Global\.

Вопрос о том, входит ли поведение на общем ПК/в RDS в требования, — это тема, позднее обнаружение которой в ходе реализации приводит к большому объёму переделок. Мы настоятельно рекомендуем при определении требований уточнить: «кто будет использовать это приложение, на каких машинах и сколько человек одновременно». Если вы планируете развернуть существующее приложение в среде общего ПК или RDS, либо вам нужно расследовать ошибку, при которой «что-то идёт не так, когда приложение используют несколько пользователей», обычно быстрее провести диагностику, глядя на реальную конфигурацию, — обращайтесь к нам.

Похожие статьи

Смежные направления консалтинга

KomuraSoft LLC (合同会社小村ソフト) занимается ревью проектирования приложений Windows с прицелом на возможное развёртывание на общих ПК или в средах RDS, проектированием разделения службы и IPC, а также расследованием первопричин ошибок, специфичных для одновременного использования несколькими пользователями.

Справочные ссылки

  1. Microsoft Learn, Service Changes for Windows Vista. О предпосылках введения изоляции Session 0, возникающей из-за неё невозможности обмена оконными сообщениями между службами и приложениями, а также о функции WTSSendMessage как альтернативе.  2 3

  2. Microsoft Learn, Interactive Services. О причинах, по которым служба не может напрямую взаимодействовать с пользователем, о конструкции с использованием CreateProcessAsUser для запуска отдельного GUI-процесса и координации через IPC, а также о рекомендации различать имена каналов по ID сеанса.  2 3

  3. Microsoft Learn, Windows Enterprise multi-session FAQ. О возможности multi-session — удержании нескольких интерактивных сеансов одновременно, — ранее доступной только на Windows Server, а теперь предлагаемой исключительно для Azure Virtual Desktop.  2 3

  4. Microsoft Learn, Kernel object namespaces. О пространстве имён по сеансам для именованных объектов ядра, о префиксах Global\ / Local\, и о том, что для создания нового объекта отображения файла или символьной ссылки в глобальном пространстве имён требуется привилегия SeCreateGlobalPrivilege 2 3 4

  5. Microsoft Learn, Named Pipes. О том, что именованные каналы доступны из любого процесса в пределах проверок безопасности и достижимы удалённо, если запущена служба сервера (основа того, почему это иной механизм, отличный от пространств имён сеанса Global\/Local\, используемых мьютексами и т. д.). 

  6. Microsoft Learn, GetSystemMetrics function (winuser.h). О спецификации определения удалённого сеанса через SM_REMOTESESSION 2

  7. Microsoft Learn, Detecting the Remote Desktop Services environment. О примере кода для GetSystemMetrics(SM_REMOTESESSION), об ограничении, при котором эта функция ошибочно определяет удалённый сеанс как локальный при использовании RemoteFX vGPU, и об альтернативном определении через ключ реестра GlassSessionId 2

  8. Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). Об уязвимости повышения привилегий, эксплуатирующей недостаток обработки оконных сообщений в драйвере режима ядра Windows (win32k.sys) (один из примеров этого класса атак через оконные сообщения). 

  9. Microsoft Learn, schtasks create. О том, что у учётной записи SYSTEM нет прав интерактивного входа, и пользователь не может видеть программу или задачу, работающую с привилегиями SYSTEM, и взаимодействовать с ней. 

  10. Microsoft Learn, Elevated Task Model. О конфигурации, при которой приложение обычного пользователя выполняет операции, требующие прав администратора, через задачу, зарегистрированную как SYSTEM, но с дескриптором безопасности, настроенным так, чтобы обычный пользователь всё же мог её запустить. 

  11. Microsoft Learn, Remote Desktop Services overview in Windows Server. О том, что RDS — это основа, предоставляющая многосеансовые, основанные на сеансах рабочие столы.  2

  12. Microsoft Learn, Troubleshoot Remote desktop disconnected errors. О том, что до двух одновременных удалённых подключений возможны в административных целях без лицензии RDS CAL, а сверх этого требуются роль RD Session Host и надлежащие лицензии RDS CAL.  2

  13. Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). О различии между моделями лицензирования RDS CAL на устройство и на пользователя, а также о механизме выдачи/отслеживания сервера лицензирования.  2

  14. Microsoft Learn, qwinsta. О том, что столбец SESSIONNAME показывает имя, присвоенное сеансу (console для консоли или имя, начинающееся с rdp-tcp#, для RDP-подключения). 

  15. Microsoft Learn, SystemParameters.IsRemoteSession Property. О свойстве, предоставляющем из WPF проверку, эквивалентную SM_REMOTESESSION

  16. Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). О получении ID сеанса, подключённого к физической консоли, и о возврате 0xFFFFFFFF во время перехода.  2

  17. Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). О возможности перечислить все сеансы на сервере RD Session Host. 

  18. Microsoft Learn, Client/Server application guidelines. О том, почему «подключение с одной машины» нельзя приравнивать к «одному пользовательскому сеансу», и об идентификации сеансов через ProcessIdToSessionId

  19. Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). О том, что Remote Desktop Services по умолчанию создаёт отдельную временную папку для каждого сеанса (внутри профиля пользователя, с ID сеанса в имени). 

  20. Microsoft Learn, Peripheral hardware guidelines. О том, как диски и принтеры на стороне клиента отображаются из сеанса на сервере RD Session Host в зависимости от конфигурации перенаправления, и о механизме, позволяющем драйверу устройства отключить перенаправление. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification

Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...

Японская эра, праздники и даты закрытия периода в бизнес-приложениях — устойчивый к смене эры дизайн, JapaneseCalendar и расчёт рабочих дней на практике

Показать «Рэйва 8» в отчёте, рассчитать рабочие дни без учёта праздников, оплатить в последний рабочий день месяца, следующего за закрыти...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Почему служба Windows не может показать диалоговое окно?
Начиная с Windows Vista службы выполняются в выделенном Session 0, изолированном от интерактивного сеанса любого вошедшего в систему пользователя, поэтому интерфейс, поднятый службой, невидим для всех — а вызов MessageBox может навсегда подвесить процесс в ожидании нажатия кнопки OK, которую никто не может нажать. Эта изоляция — решение по безопасности, которое структурно блокирует обмен оконными сообщениями через границу привилегий, закрывая целый класс атак с повышением привилегий. Официально рекомендованное решение — вынести интерфейс в отдельный процесс, работающий в сеансе пользователя, и общаться со службой через IPC, например именованные каналы; запуск программы с интерфейсом от имени интерактивного пользователя через Планировщик заданий — ещё один задокументированный вариант.
Могут ли несколько пользователей одновременно подключаться к Windows 10 или 11 по RDP?
Нет — обычные клиентские ОС, такие как Windows 10/11 Pro или Enterprise, принципиально однопользовательские (single-session), поэтому сеанс физической консоли и удалённое подключение в основном не могут сосуществовать; подключение по RDP переключает локальный экран на экран блокировки. Несколько одновременных интерактивных сеансов требуют Windows Server с ролью Remote Desktop Services, для чего, помимо двух административных подключений, нужны роль RD Session Host и лицензии RDS CAL. Единственное клиентское издание с поддержкой множества сеансов, Windows Enterprise multi-session, предлагается исключительно для Azure Virtual Desktop.
Почему мой мьютекс единственного экземпляра не мешает запуску из сеансов других пользователей?
Именованные объекты ядра, такие как мьютексы, события и семафоры, имеют собственное пространство имён для каждого сеанса. Мьютекс, созданный без префикса, попадает в пространство имён текущего сеанса, поэтому сеанс другого пользователя (или даже второй сеанс того же пользователя) воспринимается как совершенно новый, отдельный объект, и защита единственного экземпляра незаметно перестаёт работать. Если нужен один экземпляр на всю машину и во всех сеансах, имя нужно явно снабдить префиксом Global\. Если нужен один экземпляр на пользователя, охватывающий все его сеансы, объедините Global\ с SID пользователя, встроенным в имя.
Как моё приложение может определить, работает ли оно в сеансе удалённого рабочего стола?
Стандартная проверка — это Win32 API GetSystemMetrics(SM_REMOTESESSION), которая возвращает ненулевое значение для удалённого сеанса; WPF предоставляет ту же проверку через SystemParameters.IsRemoteSession. Учтите, что в удалённых сеансах с RemoteFX vGPU эта функция ошибочно определяет удалённый сеанс как локальный, и задокументированный обходной путь — сравнить значение GlassSessionId из реестра с текущим ID сеанса. Переменная окружения SESSIONNAME (Console против RDP-Tcp#...) подходит для быстрой диагностики, но не является официальным API. Не используйте WTSGetActiveConsoleSessionId для определения RDP-сеансов — она возвращает только сеанс, подключённый к физической консоли.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог