Владимир Юдин
Обсуждение данной темы ведется, начиная с появления первых 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.
Языки программирования: разное
|