Сообщений 13    Оценка 350 [+1/-0]         Оценить  
Система Orphus

Как избежать запуска нескольких экземпляров приложения

Автор: Dr. Joseph M. Newcomer
Перевод: Алексей Остапенко
Источник: Avoiding Multiple Instances of an Application
Опубликовано: 17.02.2001
Исправлено: 24.11.2005
Версия текста: 1.1

Обзор

Существует множество причин, по которым зачастую желательно ограничить число работающих на данной машине экземпляров (instance) программы до одного. Есть множество кустарных способов, позволяющих это сделать, но большая их часть неверна.

Уж я то это знаю. Я использовал один такой сомнительный способ годами. Я даже привел его в книге "Win32 Programming". И теперь я чувствую себя смущенным.

Данный очерк рассматривает некоторые из возможных решений и указывает на проблемы тех из них, которые не будут работать, и на те условия, в которых они дадут сбой.

Реальную опасность представляет то, что Microsoft не только не документировала корректный способ, но и предлагает два примера фатально ошибочного кода, которые я детально рассматриваю.

Что такое "несколько экземпляров"?

Одним из интересных моментов, который отметил один из читателей, Даниель Ломанн (Daniel Lohmann), состоит в том, что понятие "несколько экземпляров" не было четко определено в ранних вариантах данной статьи. Он указывает, что термин "несколько экземпляров" может означать различные вещи в многопользовательской среде NT. Он говорит о возможности использования таких вызовов API, как LogonUser, CreateProcessAsUser, CreateDesktop и SwitchDesktop, которые позволяют множеству пользовательских (user) и экранных (desktop) сессий (sessions) одновременно выполняться под различными учетными записями (accounts).

Таким образом, существует несколько вариантов, в которых может быть нежелателен запуск множества экземпляров программы. Даниель классифицирует их следующим образом:

  1. Запрет запуска нескольких экземпляров в одной пользовательской сессии безотносительно к тому, сколько у нее рабочих столов, и разрешение запуска нескольких копий программы в разных пользовательских сессиях.
  2. Запрет запуска нескольких экземпляров на одном рабочем столе, но разрешается одновременное выполнение нескольких экземпляров приложения на разных рабочих столах.
  3. Запрет запуска нескольких экземпляров приложения под одной учетной записью, вне зависимости от количества рабочих столов и пользовательских сессий, работающих под этой учетной записью. При этом несколько копий программы могут работать в сессиях, запущенных под разными учетными записями.
  4. Запрет запуска нескольких экземпляров приложения на одной машине. При этом не имеет значения ни число рабочих столов, ни число работающих пользовательских сессий - возможна работа только одной копии программы.

Он замечает, что большинство программистов подразумевают (a), а иногда (b) или (c). Метод, приведенный в ранних вариантах этой статьи, всегда интерпретировал вопрос как (d). Заметим, что в случае с Win9x/Millenium, существует только вариант (d), поскольку на машине не может быть более одной пользовательской сессии и более одного рабочего стола.

Поэтому, когда вы прочитаете следующий ниже абзац, имейте в виду, что описываемый метод дает решение только для случая (d). Далее, я представлю предлагаемое Даниелем решение для расширения данного метода на случаи (a)-(c). Он отмечает, что определенные "ориентированные на сервис" приложения, попадающие в класс (d), часто лучше реализуются как системные сервисы. Сервис может быть запущен по требованию или при инициализации системы, и все пользователи и все рабочие столы будут использовать один общий сервис.

Правильное решение: использование объекта ядра

Следующий код является адаптацией примера, посланного в группу новостей microsoft.public.mfc Дэвидом Лоунде (David Lowndes). Его пример использовал вариант с FindWindow, который не будет надежно работать по причинам, излагаемым ниже. Вместо этого, моя версия использует иной способ обнаружения окна другого экземпляра программы, который я считаю более надежным. Его код также использует разделяемую (shared) переменную, которая вызывает другую проблему, рассматриваемую далее. Однако вы можете использовать более простой способ, если считаете эту проблему несущественной. Данное решение является общим, на нем строятся решения для многоэкранного и многопользовательского вариантов; основное отличие состоит в методе формирования уникального идентификатора, который обсуждается позже.

В следующем ниже примере вы должны декларировать функцию CMyApp::searcher как статический член класса, например:

static BOOL CALLBACK searcher(HWND hWnd, LPARAM lParam);

Также необходимо определить зарегистрированное оконное сообщение, например, UWM_ARE_YOU_ME. Подробности смотрите в моей статье "Управление Сообщениями". В определении класса CMainFrame добавьте обработчик этого сообщения:

afx_msg LRESULT OnAreYouMe(WPARAM, LPARAM);

и добавьте запись в MESSAGE_MAP

ON_REGISTERED_MESSAGE(UWM_ARE_YOU_ME, OnAreYouMe)

реализуйте обработчик следующим образом:

LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM)
{
    return UWM_ARE_YOU_ME;
} // CMainFrame::OnAreYouMe

Теперь можно реализовать метод searcher, ищущий нужное окно.

BOOL CALLBACK CMyApp::searcher(HWND hWnd, LPARAM lParam)
{
    DWORD result;
    LRESULT ok = ::SendMessageTimeout(hWnd,UWM_ARE_YOU_ME,0,0,
                                      SMTO_BLOCK | SMTO_ABORT_IF_HUNG,200,
                                      &result);
    if(ok == 0)
    return TRUE; // игнорируем это окно и продолжаем

    if(result == UWM_ARE_YOU_ME)
    { /* нашли */
        HWND * target = (HWND *)lParam;
        *target = hWnd;
        return FALSE; // заканчиваем поиск
    } /* нашли */

    return TRUE; // продолжаем поиск
} // CMyApp::searcher

//--------------------------------------------------------

BOOL CMyApp::InitInstance()
{
    bool AlreadyRunning;
    HANDLE hMutexOneInstance = ::CreateMutex( NULL, FALSE,
        _T("MYAPPNAME-088FA840-B10D-11D3-BC36-006067709674"));

    // в случае альтернативных решений
    // UID в предыдущем вызове замещается
    // вызовом createExclusionName

    AlreadyRunning = ( ::GetLastError() == ERROR_ALREADY_EXISTS || 
                       ::GetLastError() == ERROR_ACCESS_DENIED);
    // вызов возвращает ERROR_ACCESS_DENIED, если мьютекс был создан
    // в другой пользовательской сессии, т.к. в качестве параметра
    // SECURITY_ATTRIBUTES при создании мьютекса передается NULL

    if ( AlreadyRunning )
    { /* активизируем окно работающего экземпляра */

        HWND hOther = NULL;
        EnumWindows(searcher, (LPARAM)&hOther);

        if ( hOther != NULL )
        { /* помещаем окно поверх других */
            ::SetForegroundWindow( hOther );
            if ( IsIconic( hOther ) )
            { /* разворачиваем окно */
                ::ShowWindow( hOther, SW_RESTORE );
            } /* разворачиваем окно */
        } /* помещаем окно поверх других */

    return FALSE; // прекращаем запуск
    } /* активизируем окно работающего экземпляра */
    // ... продолжение InitInstance

    return TRUE;
} // CMyApp::InitInstance

Основная проблема: "Состязания выполнения (Race Conditions)"

После того, как вы прочитаете раздел про "Состязания выполнения", вы можете увидеть, что этот код допускает такое "состязание". Если мьютекс был создан экземпляром 1, который все еще запускается (еще не создал основное окно), и экземпляр 2 обнаруживает, что мьютекс уже существует, он пытается найти окно экземпляра 1. Но поскольку экземпляр 1 еще не закончил инициализацию, данный код не приведет к всплыванию окна 1-го экземпляра. Но это нормально. Поскольку 1-ый экземпляр еще не создал окно, он продолжит его создание, и оно появится на рабочем столе, как и ожидалось.

В любой системе с потенциально возможным параллельным или псевдопараллельным исполнением потоков возможно явление, называемое "состязание выполнения". Это ситуация, в которой корректность взаимодействия двух (или, обобщенно, более) процессов определяется отношениями времен выполнения определенных частей программ. Если программа A достигает определенной точки выполнения раньше, чем программа B проделала все необходимые действия, программа A может сбойнуть.

Многие решения для "поиска другого исполняющегося экземпляра программы" сбоят из-за "состязаний выполнения". Мы рассмотрим этот вопрос после того, как взглянем на способ, который, казалось бы, должен работать.

Почему не работает FindWindow

Одно из распространенных народных мнений гласит, что для запрета выполнения множества экземпляров программы можно использовать функцию API FindWindow. Использование этой функции очень опасно. Ваше приложение имеет отличные шансы навсегда зависнуть при выполнении этого вызова. В любом случае, даже если не существует условий, которые привели бы к зависанию, методы, использующие данную функцию, настолько глубоко ошибочны, что они вообще не имеют шансов нормально работать. Существует два сценария сбоя: приложение не запускается или одновременно запускаются несколько экземпляров. Оба варианта абсолютно неудовлетворительны.

К сожалению, данная кустарщина настолько популярна, что в нее верят даже в Microsoft. В статье Q109175 из KB, предлагают скачать программу onetime.exe, которая является sfx-zip архивом, содержащим пример, основанный именно на таком подходе. Статья Q141752 из KB и сопутствующий ей пример onet32.exe продолжают распространять этот миф. Поверьте мне. Я сам через это прошел, и сам использовал такой подход. Он не работает! Не используйте его!

Но почему он не работает? Ну, в идеале этот метод срабатывает. Но когда возникает крошечное, незначительное и несерьезное отклонение, моментально наступает сбой, по крайней мере, с точки зрения конечного пользователя. Более того, данное отклонение не является неисправностью. К сбою может привести совершенно корректно работающая программа.

Прототип FindWindow выглядит так:

HWND CWnd::FindWindow(LPCTSTR classname, LPCTSTR caption)

Параметр classname может быть равен либо NULL, что позволяет искать окна с произвольным оконным классом, либо имени оконного класса (внутренний вызов API также допускает передачу HATOM, приведенного к LPCTSTR, но получение HATOM через вызов ::RegisterWindowClass редко встречается в MFC). Параметр caption - это строка, которая сравнивается с заголовком каждого просматриваемого окна. Функция возвращает HWND первого окна, у которого имя оконного класса соответствует classname, а заголовок совпадает с caption.

И почему это не будет работать?

Итак, если вы не зарегистрировали оконный класс своего приложения с помощью AfxRegisterClass, то вы не знаете имени оконного класса. Но вы говорите: "Все нормально, я просто использую NULL, и я знаю заголовок окна".

Но вы его не знаете!

Ну, возможно, что знаете. На этой неделе. В этой стране. Для данной версии. Вероятно. Пока, это не MDI-приложение.

Значит, по определенной причине вы не желаете использовать константу в качестве значения caption. Это было бы очень неудачной идеей. Потому что, если вы будете локализовывать свое приложение, то вам придется менять заголовок окна. И вы в проигрыше.

Ага! Вы поместите заголовок в файл ресурсов. Затем вы используете LoadString, чтобы его извлечь. Ну, все хорошо, но вы должны позаботиться, чтобы такой же LoadString использовался для установки заголовка. Хорошо, возможно, что это сработает. Для диалогового приложения. Но для других приложений в заголовок будет включаться имя входного файла, и вы не можете его предсказать, поэтому FindWindow тут уже бесполезна.

Итак, вы не можете использовать FindWindow, если вы ищите заголовок.

Но в действительности все еще хуже. На самом деле FindWindow вызывает EnumWindows, и для каждого найденного дескриптора окна верхнего уровня (top-level) вызывается функция GetWindowText. Она реализуется посылкой окну сообщения WM_GETTEXT. Но если поток, владеющий окном, блокирован на Семафоре, Мьютексе, Событии, операции ввода-вывода, или каким-либо иным образом, то вызов SendMessage приведет к блокировке до тех пор, пока поток, владеющий окном, не освободится и не продолжит выполнение. Но это может никогда не произойти. Таким образом, вызов FindWindow приведет к пожизненной блокировке, и ваше приложение никогда не запустится.

Когда вам приходится проехать значительное расстояние, потому что ваш лучший клиент не может запустить ваше приложение на своей машине, вы желаете убедиться, что (a) такое больше никогда с ним не случится и (b) однозначно никогда не случится ни с одним из его клиентов. На поиск такой ошибки уходит много времени.

Итак, вы говорите: "Это действительно плохая идея. Я буду использовать имя оконного класса. В конце концов, это именно то, что Microsoft делает в своих примерах, и они Должны Знать Что Они Делают.

Код, приводимый Microsoft в качестве примера, ошибочен. И внешние недочеты скрывают глубинные дефекты. Например, имя оконного класса задается в виде текстовой строки. И, что отнюдь не очевидно, вы однозначно должны модифицировать это имя так, чтобы оно было уникальным. Уникальным глобально. Таким, что никакой другой разработчик в любом другом месте известной нам Вселенной никогда не стал бы использовать такое же имя.

Теперь я поведаю небольшую историю, которую вам стоило бы принять близко к сердцу: давным-давно, я написал Win16-приложение. Оно регистрировало оконный класс "generic" потому, что я скопировал его со стандартного примера Microsoft. И я получил жалобу: "Ваша программа сбоит". Угадайте почему? Приложение пыталось зарегистрировать оконный класс "generic", который так же использовал кто-то еще, скопировавший свою программу со стандартного примера. В те времена имена оконных классов были глобальными для всей системы, поэтому приложение не смогло зарегистрировать класс.

Вы говорите, что сейчас эти имена не глобально уникальны, и совершенно спокойно можно использовать программу A, регистрирующую класс "MainWindow", и программу B, регистрирующую класс с тем же именем. Это верно. Но только тогда, когда ни одной из них нет дела до другой. Если программа B начнет выяснять имя класса какого-либо окна, то она не сможет определить, зарегистрировано ли имя "MainWindow" ее собственным экземпляром или какой-то другой программой, которая никогда не имела представления о программе B.

Итак, первое, что необходимо сделать - это добиться уникальности имени класса, потому что, хотя имена и не глобальны, вы собираетесь искать данное имя класса, и вы не можете определить, с которым классом из множества зарегистрированных экземпляров классов с одним и тем же именем вы имеете дело.

Отлично, пусть вы прочитали другие мои очерки и знаете, как это сделать, используя GUIDGEN. Он создает 128-битный номер, записанный в виде строки из шестнадцатеричных цифр, и известно, что этот номер глобально уникален. И вы просто добавляете этот номер к дружественному имени класса. Супер! Теперь вы знаете, что имя уникально, и вы собираетесь искать по этому имени. Т.к. вы не попадаете в ловушку с WM_GETTEXT, то не имеет значения, подвисло ли другое приложение по какой-либо причине, поскольку, в конце концов, для того, чтобы вы могли получить информацию о классе его окна, не требуется его выполнения.

Угадайте, что теперь? Вы только что попали в ловушку "состязания выполнения".

Вот основной код одного из примеров Microsoft. Я привел только Хорошие Части:

LPCTSTR lpszUniqueClass = _T("MyNewClass");

//----------------------------------------------------------------

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    // Используем специфическое имя класса, которое мы определили ранее
    cs.lpszClass = lpszUniqueClass; // [A]

    return CMDIFrameWnd::PreCreateWindow(cs);
}

//----------------------------------------------------------------

BOOL COneT32App::InitInstance()
{
    // Если уже есть работающий экземпляр приложения,
    // то активизируем его и возвращаем FALSE из InitInstance
    // чтобы завершить выполнение данной копии.

    if(!FirstInstance())// [B]

    return FALSE;

    // Регистрируем свое уникальное имя класса, которое мы желаем использовать
    WNDCLASS wndcls;

    memset(&wndcls, 0, sizeof(WNDCLASS));   // устанавливаем все параметры в NULL по-умолчанию

    wndcls.style = ...;
    ... здесь инициализируем другие переменные структуры wndcls,
	... несущественные для данного примера

    // Устанавливаем свое собственное имя класса для последующего использования FindWindow
    wndcls.lpszClassName = lpszUniqueClass;

    // Регистрируем новый класс и выходим, если это не удалось сделать
    if(!AfxRegisterClass(&wndcls)) // [C]
    {
        return FALSE;
    }
    ... продолжение InitInstance...
    CMainFrame* pMainFrame = new CMainFrame; // [D]
    ... все остальное

    return TRUE;
}

//----------------------------------------------------------------

BOOL COneT32App::FirstInstance()
{
    CWnd *pWndPrev, *pWndChild;
  
    // Определяем, существует ли окно с нашим именем класса...
    if (pWndPrev = CWnd::FindWindow(lpszUniqueClass,NULL)) // [E]
}

//----------------------------------------------------------------

Приложение будет нормально инициализироваться, исполняя код так, что последовательность, обозначенная комментариями // [x] (которые расположены по алфавиту от начала кода к концу, хотя код и не выполняется в таком порядке), такова:

[B]-[E]-[C]-[D]-[A]

Рассмотрим приведенную ниже последовательность инициализации двух приложений.

Время Экземпляр 1 Экземпляр 2
1 B FirstInstance()
2 E FindWindow() => FALSE
3 B FirstInstance()
4 E FindWindow() => FALSE
5 C AfxRegisterClass()
6 C AfxRegisterClass()
7 D new CMainFrame
8 D new CMainFrame
9 A (в PreCreateWindow)
10 A (в PreCreateWindow)

Заметьте, что этот пример благополучно проходит тест. В момент времени 2, когда экземпляр 1 вызывает FindWindow, нет других экземпляров окна. Поэтому FindWindow возвращает FALSE, указывая, что второй запущенной копии нет. Итак, процесс инициализации продолжается. Но, между тем инициализируется экземпляр 2. В момент времени 4, когда он исполняет FindWindow, другие экземпляры окна искомого типа отсутствуют. Поэтому FindWindow возвращает FALSE, и экземпляр 2 знает, что он единственный. И он продолжает инициализацию. Итак, к тому моменту времени, когда экземпляр 1 создает окно, которое могло бы быть найдено FindWindow, тест уже давным-давно прошел то место, когда экземпляр 2 мог обнаружить это окно. И мы получаем две работающих копии.

Если вы думаете, что такое не может произойти, значит, вы живет в мире, отличном от реального. Я видел, как такое происходит. Неоднократно. Примерно один раз из трех.

Я обнаружил это, когда у клиента был очень быстрый "Пентиум", и он сконфигурировал рабочий стол Win98 на запуск приложений по одному клику.

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

Ладно, а почему работает метод с CreateMutex? Не страдает ли он от той же самой проблемы? Нет, поскольку создание Мьютекса - атомарная операция ядра. Создание Мьютекса гарантированно завершится до того, как любой другой поток сможет успешно создать Мьютекс. Следовательно, у нас есть полная гарантия того, что создание объекта и проверка его существования - это одна операция, не разделенная, как FindWindow или (как я рассматриваю в следующем разделе) SendMessage, тысячами или десятками тысяч прерываемых инструкций.

Даниель Ломанн, внесший существенный вклад в эту статью, также указывает, что, в терминах "уникальности", FindWindow имеет проблему в том, что она перебирает только окна, принадлежащие рабочему столу, к которому привязан вызывающий поток. Следовательно, если существует другой экземпляр, исполняющийся на другом рабочем столе, то вы не сможете его найти, чтобы сделать его активным.

SendMessage: "Соревнование выполнения"

Один из наиболее распространенных кустарных методов (и, увы, его же я использовал годами) - использование EnumWindows, вызов SendMessage для каждого окна, и анализ кода возврата из SendMessage. Вы посылаете зарегистрированное оконное сообщение (см. мой обзор "Управление Сообщениями"), и, если вы получаете это сообщение, то возвращаете TRUE. Все остальные окна не распознают это сообщение и возвратят FALSE. Этот способ оказывается глубоко ошибочным по ряду причин.

Заметим, что этот способ всегда работал в среде Win16, т.к. она использовала кооперативную многозадачность. Именно вытесняющая многозадачность Win32 приводит к провалу этого метода. И она действительно это делает.

Вызов SendMessage может подвиснуть на неопределенное время. И если поток, владеющий окном, блокирован на Семафоре, Мьютексе, Событии, операции ввода-вывода или каким-либо иным образом, то вызов SendMessage приведет к блокировке до тех пор, пока этот поток не освободится и не продолжит работу. Но это может никогда не произойти. Итак, вы не добились никакой надежности.

Вы можете решить эту проблему, используя SendMessageTimeout. Это более-менее работает. Обычно, вы выберете короткий тайм-аут, например, 200 мс.

Но дальше - больше.

В Microsoft, нарушив все известные спецификации Windows, создали приложение, которое не передает неизвестные ему сообщения в DefWindowProc, которая должна вернуть 0 на любое неопознанное ей сообщение. Вместо этого, они используют компонент, вероятно связанный с Personal Web Server, обладающий по-настоящему антиобщественным свойством возвращать 1 на любое сообщение, независимо от того, распознает он его или нет. Поэтому, вы не можете положиться на то, что ноль соответствует не вашему приложению.

Ну, эту проблему можно решить. Когда мне пришлось сделать это по другой причине, я пришел к тому, чтобы возвращать значение зарегистрированного оконного сообщения, обходя просчет Microsoft.

Итак, мы решили проблему тайм-аутов и ложных сообщений. Мы знаем, что не стоит полагаться на содержимое заголовка, и, вероятно, стараемся избегать использования имени оконного класса. Ну, так почему же ничего не работает?

А все из-за гораздо более существенной проблемы: "состязания выполнения". В точности той же, что и описанная в предыдущем разделе.

Данный код выполняет цикл EnumWindows и для каждого HWND вызывает SendMessage. И вот что произошло: экземпляр приложения 1 искал другой экземпляр, не нашел его и продолжил инициализацию. В то же время, экземпляр 2 так же искал свою копию, но, поскольку первый экземпляр еще не запустился и не создал свое главное окно, не нашел конфликтующей копии и продолжил запуск.

Используя схему, аналогичную таблице I, которую я использовал для демонстрации того, почему не работает базовый метод с FindWindow, вы можете показать, что этот способ не сработает по той же причине.

Это наиболее существенный провал этого метода и причина, по которой его нельзя использовать.

Разделяемая переменная: Другая проблема

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

#pragma comment(linker, "/SECTION:.shr,RWS")
#pragma data_seg(".shr")
HWND hGlobal = NULL;
#pragma data_seg()

// в коде инициализации:
// g_hWnd устанавлвается после создания главного окна.

BOOL CMyApp::InitInstance()
{
    bool AlreadyRunning;

    HANDLE hMutexOneInstance = ::CreateMutex( NULL, TRUE,
        _T("MYAPPNAME-088FA840-B10D-11D3-BC36-006067709674"));

    AlreadyRunning = (GetLastError() == ERROR_ALREADY_EXISTS);

    if (hMutexOneInstance != NULL) 
    {
        ::ReleaseMutex(hMutexOneInstance);
    }
    if ( AlreadyRunning )
    { /* активизируем окно работающего экземпляра */
        HWND hOther = g_hWnd;
        if (hOther != NULL)
        { /* помещаем окно поверх других */
            ::SetForegroundWindow(hOther);
            if (IsIconic(hOther))
            { /* разворачиваем окно */
                ::ShowWindow(hOther, SW_RESTORE);
            } /* разворачиваем окно */
        } /* помещаем окно поверх других */

        return FALSE; // заканчиваем выполнение
    } /* активизируем окно работающего экземпляра */

    // ... продолжение InitInstance

    return TRUE;
} // CMyApp::InitInstance

Этот способ почти работает. Он избегает фундаментального "состязания выполнения", поскольку CreateMutex является атомарной операцией. Вне зависимости от относительной синхронизированности двух процессов, только один из них создаст Мьютекс первым, а другой получит ERROR_ALREADY_EXISTS. Заметим, что я использовал GUIDGEN для получения гарантированно уникального ID.

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

Однако, этот код проще моего; он не требует ни обработчика EnumWindows, ни кода внутри него, ни зарегистрированного пользовательского оконного сообщения, ни обработчика в CMainFrame.

Я не понимаю, почему Мьютекс создается в собственном (owned) режиме (второй параметр равен TRUE). В документации Microsoft даже говорится, что при выполнении CreateMutex из различных потоков, этот параметр должен всегда равняться FALSE, иначе невозможно определить, какой поток в действительности владеет Мьютексом. Поскольку этот Мьютекс никак не используется в коде, использование TRUE в качестве параметра, похоже, не имеет никакого значения.

Даниель Ломанн заметил, что разделяемая память разделяется даже в том случае, когда процессы исполняются под разными учетными записями, при условии, что они выполняются на одной машине. И, несомненно, она разделяется, если экземпляры связаны с разными рабочими столами. Следовательно, использование разделяемой переменной имеет ограничения и может даже оказаться вредным, когда вы обобщаете понятие "уникального экземпляра", как отмечает Даниель в своих рекомендациях в следующем разделе.

Обобщение Решения для NT

Даниель Ломанн указал на фундаментальный недостаток описанного выше метода. Хотя он и надежен, он не полон в том смысле, что он охватывает только одно из четырех возможных значений понятия "уникального экземпляра":

  1. Запрет запуска нескольких экземпляров в одной пользовательской сессии.
  2. Запрет запуска нескольких экземпляров на одном рабочем столе.
  3. Запрет запуска нескольких экземпляров приложения под одной учетной записью.
  4. Запрет запуска нескольких экземпляров приложения на одной машине.

В частности, он указывает, что мой способ создания имени Мьютекса использует глобальное для системы имя, гарантированно уникальное для приложения, но известное всем пользователям, сессиям и рабочим столам. Заметим, что это замечание относится к любому использованию глобальных имен для Мьютексов, Семафоров, Событий и даже разделяемых отображаемых в память файлов: предположение о правильности использования одного имени основывается на вашей интерпретации вышеуказанных четырех пунктов. Так, если вы строите систему, использующую синхронизационные примитивы, и требующую решения, отличного от (d), вам также придется применить изложенные ниже методы для именования синхронизационных примитивов.

Даниель указывает, что в NT Terminal Server edition (в Windows 2000 TS встроен) ядро более не имеет единого "глобального" пространства имен, но, в действительности, каждая сессия терминального сервера имеет свое приватное пространство имен. Системные сервисы разделяют общее пространство имен, относящееся к тому, что называют "консольной сессией". Он говорит, что "все это приводит к использованию большего количества памяти, и делает некоторые задачи программирования более коварными, но в результате любой пользователь терминального сервера может запустить почтового клиента".

Вот еще одна небольшая поправка, которую он сделал в моем коде:

Вызов CreateMutex завершается с ERROR_ACCESS_DENIED, если Мьютекс был создан в другой пользовательской сессии. Это происходит из-за передачи NULL вместо SECURITY_ATTRIBUTES, т.е. устанавливаются права доступа по умолчанию. Обычный DACL по умолчанию разрешает доступ к объекту только создателю/владельцу (CREATOR/OWNER) и системе (SYSTEM).

Предложенное Даниелем решение заключается в расширении имени Мьютекса, отталкиваясь от используемого мною метода с GUID, чтобы охватить варианты (a)-(c). Он пишет:

"Я начну с (b), т.к. это самый простой вариант. Используя GetThreadDesktop(), вы можете получить дескриптор рабочего стола, который используется вашим потоком. Передавая его в GetUserObjectInformation(), вы можете получить имя рабочего стола, которое уникально".

"Даже случай (c) довольно прост. Решение заключается в добавлении имени текущего пользователя. Используя GetUserName(), вы получаете имя учетной записи текущего пользователя. Вы должны доопределить его текущим пользовательским доменом, который может быть определен с помощью вызова GetEnvironmentVariable() для переменной USERDOMAIN."

"Вариант (a) чуть более сложен. Вы должны получить маркер (token) процесса. Передать этот маркер в GetTokenInformation(), чтобы получить структуру TOKEN_STATISTICS. Член этой структуры AuthenticationId, 64-битный номер (описанный как LUID) содержит уникальный идентификатор пользовательской сессии. Переконвертируйте его в строку".

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

exclusion.h

#define UNIQUE_TO_SYSTEM  0
#define UNIQUE_TO_DESKTOP 1
#define UNIQUE_TO_SESSION 2
#define UNIQUE_TO_TRUSTEE 3
CString createExclusionName(LPCTSTR GUID, UINT kind = UNIQUE_TO_SYSTEM);

exclusion.cpp

#include "stdafx.h"
#include "exclusion.h"
/****************************************************************************
*                             createExclusionName
* Вход:
*       LPCTSTR GUID: GUID для данного исключения
*       UINT kind: тип исключения
*               UNIQUE_TO_SYSTEM
*               UNIQUE_TO_DESKTOP
*               UNIQUE_TO_SESSION
*               UNIQUE_TO_TRUSTEE
* Результат: CString
*       Имя для мьютекса исключения
* Назначение: 
*       Создает имя для исключающего мьютекса
* Примечания:
*       GUID задается определением вида
*               #define UNIQUE _T("MyAppName-{44E678F7-DA79-11d3-9FE9-006067718D04}")
****************************************************************************/

CString createExclusionName(LPCTSTR GUID, UINT kind)
{
    switch(kind)
    { /* тип */

        case UNIQUE_TO_SYSTEM:
            return CString(GUID);

        case UNIQUE_TO_DESKTOP:
        { /* рабочий стол */
            CString s = GUID;
            DWORD len;
            HDESK desktop = GetThreadDesktop(GetCurrentThreadId());
            BOOL result = GetUserObjectInformation(desktop, UOI_NAME, NULL, 0, &len);
            DWORD err = ::GetLastError();
            if(!result && err == ERROR_INSUFFICIENT_BUFFER)
            { /* NT/2000 */
                LPBYTE data = new BYTE[len];
                result = GetUserObjectInformation(desktop, UOI_NAME, data, len, &len);
                s += _T("-");
                s += (LPCTSTR)data;
                delete [ ] data;
            } /* NT/2000 */
            else
            { /* Win9x */
                s += _T("-Win9x");
            } /* Win9x */ 

            return s;
        } /* рабочий стол */

        case UNIQUE_TO_SESSION:
        { /* сессия */
            CString s = GUID;
            HANDLE token;
            DWORD len;
            BOOL result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token);
            if(result)
            { /* NT */
                GetTokenInformation(token, TokenStatistics, NULL, 0, &len);
                LPBYTE data = new BYTE[len];
                GetTokenInformation(token, TokenStatistics, data, len, &len);
                LUID uid = ((PTOKEN_STATISTICS)data)->AuthenticationId;
                delete [ ] data;
                CString t;
                t.Format(_T("-%08x%08x"), uid.HighPart, uid.LowPart);
                return s + t;
            } /* NT */

            else
            { /* 16-битная OS */

                return s;
            } /* 16-битная OS */
        } /* сессия */

        case UNIQUE_TO_TRUSTEE:
        { /* пользователь */
            CString s = GUID;
#define NAMELENGTH 64
            TCHAR userName[NAMELENGTH];
            DWORD userNameLength = NAMELENGTH;
            TCHAR domainName[NAMELENGTH];
            DWORD domainNameLength = NAMELENGTH;

            if(GetUserName(userName, &userNameLength))
            { /* получаем сетевое имя */

                // Вызовы NetApi очень прожорливы по времени
                // Этот метод получает доменное имя из
                // переменных окружения
                domainNameLength = ExpandEnvironmentStrings(_T("%USERDOMAIN%"),
                                                            domainName,
                                                            NAMELENGTH);
                CString t;
                t.Format(_T("-%s-%s"), domainName, userName);
                s += t;
            } /* получаем сетевое имя */

            return s;
        } /* пользователь */

        default:
            ASSERT(FALSE);
            break;
    } /* тип */

    return CString(GUID);
}// createExclusionName

Возвращаясь к оригинальному примеру, замените строку, которую я жестко вбил в вызов CreateMutex, на вызов createExclusionName с нужными параметрами, чтобы получить корректно отформатированное имя для Мьютекса.

Резюме

Традиционные методы обнаружения нескольких экземпляров, включая документированные Microsoft, глубоко ошибочны. Известен только один рабочий метод - создание объекта ядра, и он описан здесь в двух из множества возможных вариантов, в которых может быть использован этот метод.

Термин "уникален" должен быть четко определен; в большинстве случаев он означает "Уникален для сессии" и, возможно, "уникален для рабочего стола", а простой подход может, в действительности, помешать разным пользователям одновременно запустить приложение под NT.

Благодарности

Отдельная благодарность Даниелю Ломанну за его замечательные дополнения к статье. Версия его кода, написанная на чистом C и немного отличающаяся от моей, доступна здесь. Его сайт стоит посетить всем, кто интересуется множественными рабочими столами и терминальным сервером.


Мнение, выраженное автором в этой статье, никак не связано с мнением Microsoft.

Copyright © 2000, The Joseph M. Newcomer Co. Все права защищены.
Переведено с разрешения автора.

    Сообщений 13    Оценка 350 [+1/-0]         Оценить