DELPHISOURCE

Домой | Статьи | Книги | FAQ | Компоненты | Программы
Архив сайта | Реклама на сайте | Ссылки | Связь

Ограничение количества одновременно запущенных экземпляров приложения

 


Владимир Юдин, Мастера Delphi

Обсуждение данной темы ведется, начиная с появления первых 32-х разрядных версий Windows. Казалось бы, проблема давно уже должна быть окончательно решена, но количество вопросов в конференциях и форумах не уменьшается, хотя из книги в книгу, из FAQ'а в FAQ кочуют одни и те же варианты решения. Но не все так очевидно и просто, что подтверждает и вынесенное в эпиграф мнение известного авторитета и эксперта.

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

1. работать во всех версиях ОС;
2. быть надежным;
3. достаточно универсальным;
4. простым в реализации;
5. не давать побочных эффектов.

Каким образом можно поступить? На ум сразу приходит решение <в лоб>: почему бы не просмотреть список всех запущенных в системе процессов и не определить, запущен ли уже наш исполняемый модуль? Но почему-то именно этот <очевидный> способ практически никогда не используется. Для получения списка процессов в 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: c2001 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.

Delphisource (2006г.)
Используются технологии uCoz