div.main {margin-left: 20pt; margin-right: 20pt}
Ограничение количества одновременно запущенных экземпляров приложения
Владимир Юдин, Мастера Delphi
Обсуждение данной темы ведется, начиная с появления первых 32-х разрядных версий
Windows. Казалось бы, проблема давно уже должна быть окончательно решена, но
количество вопросов в конференциях и форумах не уменьшается, хотя из книги в книгу,
из FAQ'а в FAQ кочуют одни и те же варианты решения. Но не все так очевидно и просто,
что подтверждает и вынесенное в эпиграф мнение известного авторитета и
эксперта.
Определим требования, которым должно удовлетворять решение для того, что бы его
можно было использовать в большинстве случаев.
Очевидно, что способ должен:
работать во всех версиях ОС;
быть надежным;
достаточно универсальным;
простым в реализации;
не давать побочных эффектов.
Каким образом можно поступить?
На ум сразу приходит решение «в лоб»: почему бы не просмотреть список всех
запущенных в системе процессов и не определить, запущен ли уже наш исполняемый
модуль? Но почему-то именно этот «очевидный» способ практически никогда не
используется. Для получения списка процессов в Windows 9X используются функции
ToolHelp, а в Windows NT – PSAPI. То есть для разных версий Windows и алгоритмы
разные. Оставим этот способ для отладочных средств и просмотрщиков…
Довольно распространена проверка на наличие окна с известным заголовком. Способ
хорош тем, что если такое окно существует, то оно выводится на передний план. Однако и
тут существуют некоторые неприятные особенности, ограничивающие применение
способа. А именно, возможна реальная ситуация, когда между процедурой проверки и созданием окна происходит довольно
длительная инициализация. Экземпляр приложения, запущенный в этот промежуток
времени, не обнаружит окна и запуститься. К сказанному следует добавить, что способ
не будет работать в приложениях, меняющих заголовок окна и вообще окон не
создающих.
Раз «в лоб» нельзя, то остается стандартный способ – при запуске приложения проверить
какой-то уникальный признак и, если он не установлен, то установить, а после завершения
работы сбросить. Именно так на практике обычно и поступают.
Кандидатов на роль уникального признака довольно много, но не все одинаково пригодны
для нашей задачи.
Например, не лучшим решением будет использовать глобальные атомы. В случае
аварийного завершения приложения, когда явно не вызывается GlobalFreeAtom, они
имеют неприятную особенность оставаться в системе до ее перезагрузки.
По той же причине абсолютно неприемлемы: создание или попытка открытия какого-либо
файла, использование реестра и т.п.
Лучшими кандидатами остаются объекты ядра – они просты в использовании, быстры,
счетчик ссылок на объект, как правило, декрементируется даже при аварийном завершении процесса.
Чаще всего это Mutex и FileMapping.
Типичный код выглядит так:
program MyProgram;
uses
Windows,
Forms,
MyUnit in 'MyUnit.pas' {Form1};
{$R *.RES}
var
Mutex : THandle;
begin
Mutex := CreateMutex(nil, False, ‘MyMutex’);
if Mutex = 0 then
MessageBox(0,'Невозможно создать мьютекс', 'Ошибка',
MB_OK or MBICONSTOP)
else if GetLastError = ERROR_ALREADY_EXISTS then
MessageBox(0,'Программа уже запущена', 'Ошибка',
MB_OK or MBICONSTOP)
else
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
CloseHandle(Mutex);
end;
end.
В приведенном коде все же есть один дефект. Он никогда не проявится в приложениях
типа «Hello World!”, но может привести к очень неприятным последствиям в приложениях
серьезных. Я имею в виду инициализацию, которая происходит до процедуры проверки
при загрузке модулей. Она так же может занимать достаточно много времени. В этих
случаях также возможен запуск нескольких экземпляров приложения, что в некоторых
случаях может привести к конфликтам доступа и нехватке ресурсов (из-за чего,
собственно говоря, чаще всего проверка и производится). Даже если этого и не
произойдет, производится бесполезная работа. Отсюда вывод: процедуру проверки
необходимо производить как можно раньше, до начала всех инициализаций, и в случае
неудачи немедленно завершать приложение. Для этого помещаем процедуру проверки в
отдельный модуль, который указываем в списке используемых проектом модулей
ПЕРВЫМ:
program MyProgram;
uses
OneHinst;
Windows,
Forms,
MyUnit in 'MyUnit.pas' {Form1};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
unit OneHinst;
interface
implementation
uses
Windows;
var
Mutex : THandle;
MutexName : array[0..255] of Char;
function StopLoading : boolean;
var
L,I : integer;
begin
// В качестве уникального имени мьютекса используем полный путь
// к исполняемому файлу приложения
L := GetModuleFileName(MainInstance,MutexName,SizeOf(MutexName));
// В имени мьютекса нельзя использовать обратные слэши, поэтому
// заменяем их на прямые
for I := 0 to L - 1 do
if MutexName[I] = '' then
begin
MutexName[I] := '/';
end;
Mutex := CreateMutex(nil,false,MutexName);
Result := (Mutex = 0) or // Если мьютекс не удалось создать
(GetLastError = ERROR_ALREADY_EXISTS); // Если мьютекс уже существует
end;
procedure ShowErrMsg;
const
PROGRAM_ALREADY_RUN = 'Невозможно запустить программу';
begin
MessageBox(0,PROGRAM_ALREADY_RUN,MutexName, MB_ICONSTOP or MB_OK);
end;
initialization
if StopLoading then
begin
ShowErrMsg;
// Так как никаких инициализаций еще не производилось, то
// спокойно используем для завершения программы Halt -
// finalization все равно выполнится
halt;
end;
finalization
if Mutex 0 then
CloseHandle(Mutex);
end.
Напоследок еще пример: как ограничить количество одновременно исполняющихся
экземпляров приложения
unit LimHinst;
//****************************************************************************
//
// Author: ©2001 Vladimir G. Yudin aka y-soft
// e-mail: y-soft@mail.ru
//
// Description: Ограничение количества одновременно работающих экземпляров
// приложения.
//
// Отличие от существующих реализаций:
//
// 1. С целью исключения преждевременных инициализаций проверка
// производится в самом начале загрузки приложения, до загрузки всех модулей
// 2. Исключен возможный конфликт имен, т.к. в качестве уникального имени
// используется полный путь к исполняемому модулю
// 3. Изменением значения HinstLim можно установить любое разрешенное количество
// одновременно запущенных экземпляров приложения
// 4. Изменением WaitPause можно регулировать время ожидания
// (если установить INFINITE, то получится своеобразный вариант горячего
// резервирования)
//
// Тестировалось в WinME и WinNT 4 SP6A. Ошибки не обнаружены
//
// Usage: модуль необходимо указать ПЕРВЫМ в списке uses файла .DPR проекта
// и установить необходимые значения констант HinstLimit и WaitPause.
//
// Возможные расширения:
//
// 1. Значения HinstLimit и WaitPause хранить в INI-файле или в реестре
// 2. Значение HinstLimit менять динамически в зависимости от условий
//
// Thanks: Спасибо Юрию Зотову за указание на существование проблемы
//
// Disclaimer: Используйте совершенно свободно на свой страх и риск.
// Автор убедительно просит сообщать ему о найденных ошибках и
// внесенных усовершенствованиях.
// Всякие совпадения идей, наименований функций, процедур, переменных и
// констант считать случайными :)
//
//****************************************************************************
interface
const
//Установите необходимые значения!!!
HinstLimit = 1;
WaitPause = 50;
implementation
uses
Windows;
var
Semaphore : THandle;
SemaphoreName : array[0..255] of Char;
IncCnt : integer;
function StopLoading : boolean;
var
L,I : integer;
begin
// В качестве уникального имени семафора используем полный путь
// к исполняемому файлу приложения (по определению уникален!!!)
L := GetModuleFileName(MainInstance,SemaphoreName,SizeOf(SemaphoreName));
// В имени семафора нельзя использовать обратные слэши, поэтому
// заменяем их на прямые (или еще на что-нибудь кроме #0)
for I := 0 to L - 1 do
if SemaphoreName[I] = '' then
SemaphoreName[I] := '/';
Semaphore := CreateSemaphore(nil,HinstLimit,HinstLimit,SemaphoreName);
Result := (Semaphore = 0) or // Если семафор не удалось создать
(WaitForSingleObject(Semaphore,WaitPause) WAIT_OBJECT_0);
// Если семафор занят
end;
procedure ShowErrMsg;
const
PROGRAM_ALREADY_RUN = 'Лимит исчерпан';
begin
// Главное окно программы еще не существует, поэтому выводим MessageBox
// без владельца
MessageBox(0, PROGRAM_ALREADY_RUN, SemaphoreName, MB_ICONSTOP or
MB_OK);
end;
initialization
IncCnt := 0;
if StopLoading then
begin
ShowErrMsg;
// Так как никаких инициализаций еще не производилось, то
// спокойно используем для завершения программы Halt -
// finalization все равно выполнится
halt;
end
else
IncCnt := 1;
finalization
if Semaphore 0 then
begin
// Обязательно явно освобождаем семафор, т.к.
// автоматически его счетчик ссылок не переустанавливается
ReleaseSemaphore(Semaphore, IncCnt, nil);
// Напоследок во избежание неожиданностей освобождаем дескриптор семафора
// (так предписывает MSDN)
CloseHandle(Semaphore);
end;
end.
Автор благодарит всех, принявших участие в обсуждении на форуме
http://delphi.mastak.ru/cgi-bin/forum.pl?look=1&id=1000482255&n=0 (копия форума)
|
Внимание! Запрещается перепечатка данной
статьи или ее части без согласования с автором. Если вы хотите разместить эту
статью на своем сайте или издать в печатном виде, свяжитесь с автором.
Автор статьи: Владимир Юдин
| |
|