Вопрос:
Как предотвратить повторный запуск приложения?
Ответ:
Это один из тех вопросов, которые легко задать, но трудно ответить. Трудно не потому, что он очень уж сложный, а потому, что ответ на него требует некоторых пояснений того, что именно и почему необходимо сделать.
Для начала, следует определиться с тем, что именно Вы вкладываете в понятие "повторный запуск приложения". Например:
- Запуск одного и того же приложения на одном компьютере вне зависимости от логина пользователя
- Запуск одного и того же приложения на одном компьютере из одной сессии в терминальном режиме
- Запуск одного и того же приложения с разных компьютеров с логином одного пользователя
- Работа с базой данных из разных приложений, но с логином одного пользователя
И это далеко не полный перечень самых разных ситуаций, которые могут подразумеваться под термином "повторный запуск приложения".
Собственно, вопрос сводится к набору признаков, по которому Вы хотите идентифицировать приложение. Причем эти признаки должны быть такими, которые однозначно определяются в момент открытия приложения и не могут быть изменены в процессе работы приложения.
Именно поэтому, наиболее распространенный способ определения приложения по его заголовку не может служить таким уникальным идентификатором. По крайней мере, сам по себе. Без каких-либо дополнительных признаков. Просто потому, что заголовок окна приложения легко может быть изменен в процессе работы приложения или же один и тот же заголовок могут иметь разные приложения. Разные в том смысле, который Вы вкладываете в это понятие.
Уникальными идентификаторами могут являться: - SYS(0) – Идентификатор компьютера и логин пользователя при входе в Windows
- Логин пользователя при входе в Ваше приложение
- Дата и время последнего входа в Ваше приложение
- GetEnv("SessionName") – имя сессии, в которой запущено приложение. Имеет смысл при работе в терминальном режиме, поскольку значение, возвращаемое функцией SYS(0) будет одинаковым, но имя сессии - разным.
Вам могут понадобиться и другие идентификаторы, формируемые либо внутри Вашего приложения, либо самой операционной системой для того, чтобы однозначно определить уникальный экземпляр приложения. Какие именно – решайте сами исходя из Вашей задачи.
Замечание
Функция GetEnv() возвращает значение переменной созданной операционной системой. Полный список таких переменных и их значение можно посмотреть из режима командной строки Windows. Нажмите кнопку "Пуск" и выберите пункт меню "Выполнить". В появившемся окне введите в командной строке команду "cmd.exe" (разумеется, без кавычек) и нажмите кнопку "Ok" или "Enter". В открывшемся черном окне введите команду "set" и нажмите "Enter".
Теперь, когда Вы определились с набором признаков, идентифицирующих Ваше приложение, встает следующий вопрос: как и где, хранить этот набор, чтобы он был доступен из другого приложения?
Это "хранилище" реквизитов должно удовлетворять следующим требованиям:
1. Формироваться в момент открытия приложения
2. Быть недоступным для изменения из другого приложения в процессе работы приложения, его создавшего. Однако должно быть доступным на чтение.
3. Очищаться при закрытии приложения. Даже если закрытие приложения произошло в аварийном режиме (сбой питания)
Ну, очевидно, идеальным вариантом такого хранилища является само приложение. Точнее, набор переменных памяти, которые создает само приложение.
Однако использовать напрямую приложение затруднительно. По ряду причин. Например, при таком подходе необходимо будет просканировать вообще все открытые приложения. Ведь заранее неизвестно, какое именно приложение нам нужно найти. Есть и другие причины.
Но, тем не менее, существует довольно простое решение данной проблемы. Это использование так называемых, объектов-семафоров. Эти объекты создаются при помощи специальных API-функций и их можно "увидеть" извне приложения. Т.е. любое другое приложение может сразу определить, существует ли искомый объект или нет.
Поскольку в данном случае необходимо определить существование одного и только одного экземпляра приложения, то лучше всего использовать объект Mutex. Свое название он получил от выражения "mutually exclusive", что означает "взаимно исключающий".
Замечание
Если у Вас стоит задача допустить существование фиксированного количества одновременно запущенных экземпляров приложений, то можно использовать объект Semaphore. Но организационно это значительно сложнее.
Полное описание способов использования объекта Mutex смотрите в библиотеке MSDN на сайте Microsoft. В данном случае будет описан один единственный способ, реализующий необходимую функциональность.
Прежде, чем использовать объект Mutex в приложении, необходимо объявить API-функцию для его создания с именем CreateMutex.
Declare Integer CreateMutex In Win32API ; Integer lpMutexAttributes, ; Integer bInitialOwner, ; String lpName
lpMutexAttributes – атрибуты защиты. В данном случае не используются, поэтому значение этого параметра устанавливается в 0.
bInitialOwner – начальное состояние объекта в момент инициализации. Значение 1 означает, что приложение создавшее объект становится владельцем этого объекта. Значение 0 означает, что приложение только создает объект, но не владеет им. В данном случае всегда следует использовать значение 1.
lpName – имя объекта. Не должно содержать символа "\" и общая длина не должна превышать 260 символов (MAX_PATH). Регистр букв (большие или маленькие) имеет значение. Т.е для объекта Mutex большие и маленькие буквы – это разные буквы.
Именно имя и будет являться тем уникальным идентификатором, по значению которого и будет определяться факт существования приложения с определенными значениями реквизитов. Само имя – это символьная строка "сложенная" из значений необходимых идентификаторов. Исключая символ "\" и длиной не более 260 символов.
Замечание
В терминальном режиме все объекты ядра (kernel), в том числе и объект mutex, работают в так называемом "пространстве имен". В терминах FoxPro можно сказать, что они имеют определенную "область видимости". По умолчанию, они имеют "область видимости" Session. Это значит, что они "видны" только в той терминальной сессии, в которой и были созданы. Другая сессия (другой пользователь) уже их не видит.
Чтобы созданный объект Mutex был виден из других сессий ему надо задать пространство имен Global. Это делается добавлением префикса "Global\" к сформированному имени. Т.е. имя будет выглядеть так:
lpName = "Global\Имя_объекта"
Подробнее о пространстве имен объектов ядра в терминальном режиме читайте здесь Kernel Object Namespaces
Функция CreateMutex возвращает так называемый, "хендл" или идентификатор объекта. Целое число. Однако данная функция может, как создать объект, так и вернуть ссылку на уже существующий объект созданный ранее другим приложением.
Чтобы определить, был ли объект Mutex действительно создан или Вы получили ссылку на ранее созданный объект, следует использовать другую API-функцию с именем GetLastError.
Если эта функция вернет значение 183 (ERROR_ALREADY_EXISTS), то это и будет означать тот факт, что объект Mutex с тем же именем уже был ранее создан. Т.е. было ранее запущено приложение с теми же реквизитами.
В принципе, объект Mutex и все ссылки на него автоматически удаляются при закрытии приложения. Поэтому можно и не давать специальные команды по удалению этого объекта из памяти. Но уборка за собой является хорошим тоном в программировании. Поэтому, лучше все-таки выполнить удаление объекта Mutex при закрытии приложения.
Простейший код использования объекта Mutex для проверки факта существования ранее запущенного приложения будет выглядеть примерно так.
* Формируем идентификатор данного приложения LOCAL lcApplicationName lcApplicationName = GetEnv("SessionName") + "#"+ SYS(0) * Формируем ссылку на объект Mutex Declare Integer CreateMutex In Win32API ; Integer lpMutexAttributes, ; Integer bInitialOwner, ; String lpName PUBLIC gnMutex gnMutex = CreateMutex(0,1,m.lcApplicationName) * Проверяем факт существования объекта Mutex с тем же именем #DEFINE ERROR_ALREADY_EXISTS 183 Declare integer GetLastError In Win32API If GetLastError() = ERROR_ALREADY_EXISTS * Приложение уже запущено * Надо вывести ранее запущенное приложение на передний план * или сообщить об этом факте пользователю * и закрыть текущее приложение Do CloseMutex with .T. Return EndIf * Если объект Mutex был именно создан, то нет ранее запущенного приложения * и можно запускать текущее приложение ... READ EVENTS * По окончании работы приложения надо удалить объект Mutex, хотя это и не обязательно Do CloseMutex with .F. Return * Процедура удаления объекта Mutex Procedure CloseMutex LParameters IsExists && существует ли другое приложение * Если другое приложение существует, то удалять объект Mutex не надо * Удаление выполняется только если объект был создан именно в этом приложении If IsExists = .f. * Удаление объекта Mutex Declare integer ReleaseMutex IN Win32API Integer hMutex ReleaseMutex(m.gnMutex) EndIf * Закрытие уже не нужного хендла объекта Mutex Declare integer CloseHandle IN Kernel32 Integer hObject CloseHandle(m.gnMutex) EndProc
Однако, несмотря на все достоинства использования объекта Mutex, его использование имеет и недостатки. Дело в том, что данный объект существует только и исключительно в памяти того компьютера, в котором он был создан.
В большинстве случаев это не является проблемой, поскольку требуется проконтролировать факт повторного запуска приложения на одном и том же компьютере. Но если Вам надо контролировать факт запуска приложения, например, одного и того же пользователя, но с разных компьютеров, то использование объекта Mutex не решает проблему.
Возвращаясь к требованиям, предъявляемым к "хранилищу" реквизитов приложения по которым определяется факт запуска приложения, дополняем его еще одним требованием.
4. Оно должно быть доступным для чтения с разных компьютеров.
В принципе, можно реализовать и это требование при помощи объекта Mutex, если создавать его всегда только на том компьютере, где хранятся общие данные приложения. Т.е. создавать этот объект на сервере. Это можно реализовать, создав специальную библиотеку Com+. Но описание данной технологии выходит за рамки данной стать. Кроме того, есть другой способ.
Дело в том, что в самой среде FoxPro есть механизм взаимодействия нескольких пользователей одного приложения. Это блокировки. Ведь запись, заблокированную одним пользователем невозможно изменить другим пользователем, хотя можно прочитать из нее информацию.
Это значит, что если создать специальную таблицу, включенную в базу данных, записи которой будут содержать информацию о факте запуска приложения, то, наложив блокировку на соответствующую запись, можно дать знать другим экземплярам приложения кто уже работает с приложением.
Чтобы операции с этой служебной таблицей не мешали основной работе приложения, лучше открывать ее в отдельной сессии данных. Это можно сделать, например, используя объект Session, введенный в Visual FoxPro 6.0 Service Pack 3. Для младших версий FoxPro придется очень аккуратно следить за установкой и снятием блокировок с таблиц. Т.е. за командами UNLOCK, RLOCK(), LOCK(), SET MULTILOCKS. Хотя, можно использовать невидимую форму.
Итак, в базе данных создается еще одна таблица. Это обычная таблица, принадлежащая базе данных. Т.е. таблица, лежащая на сервере, а не на локальном компьютере пользователя.
Список полей этой таблицы является набором идентификаторов, по которым в Вашей задаче надо отделять процессы друг от друга. Это может быть, например:
SYS(0) – Идентификатор компьютера и логин пользователя при входе в Windows
Логин пользователя при входе в Ваше приложение
Дата и время последнего входа в Ваше приложение
GetEnv("SessionName") – имя сессии, в которой запущено приложение.
Теперь, при входе в Ваше приложение, первым делом открываете эту служебную таблицу и ищете запись с полным набором идентификаторов, однозначно определяющих данный процесс. Если такой записи нет, то создаете ее.
После того, как запись найдена (или создана) предпринимаете попытку ее заблокировать. Если это не удалось, то значит приложение уже запущено.
В коде это будет выглядеть примерно так.
Служебная таблица имеет примерно такую структуру
CREATE TABLE curProc (CurProcID I AutoInc, SYS0 C(128), LastTime T)
Здесь CurProcID - это код записи. В данном случае использовано тип данных Integer-Autoincrement, который был введен только в версии Visual FoxPro 8.0. Для младших версий FoxPro можно формировать код записи при помощи специальной функции NewId() или любым удобным для Вас способом.
Идентификатором процесса выступает сетевое имя компьютера SYS(0)
Тогда собственно код функции будет таким
* Запоминаю текущую сессию данных LOCAL lnSessionCurrent lnSessionCurrent = SET("DataSession") * Проверяю факт создания частной сессии данных IF Type("m.goSessionUsers") = "U" PUBLIC goSessionUsers goSessionUsers = CreateObject("Session") * Перехожу в созданную сессию данных Set DataSession To goSessionUsers.DataSessionId * Выполняю настройки в этой сессии SET REPROCESS TO 3 SET TALK OFF SET MULTILOCKS ON Else * Перехожу в частную сессию данных Set DataSession To goSessionUsers.DataSessionId EndIf * Собственно поиск и блокировка записи IF USED("curProc") = .F. USE curProc IN 0 SHARED ENDIF SELECT curProc LOCATE FOR SYS0 = SYS(0) IF FOUND()=.F. INSERT INTO curProc (SYS0) VALUES (SYS(0)) ENDIF IF RLOCK()=.F. * Запись блокирована другим процессом * Т.е. предпринята попытка повторного запуска приложения ELSE * Записываю время входа REPLACE LastTime WITH DateTime() ENDIF * Возвращаюсь в исходную сессию данных Set DataSession To m.lnSessionCurrent
Снимать блокировку записи не надо до окончания работы приложения. В случае аварийного завершения приложения (сбой питания) снятие блокировки произойдет автоматически.
Если при закрытии приложения удалять (или очищать) созданную запись, то по факту наличия не заблокированной записи можно сделать вывод о том, что приложение было закрыто аварийно. И определить кем именно.
Запись времени нужна для того, чтобы не накапливался мусор. Например, уже нет компьютера со значением SYS(0), но запись с таким значением все еще существует. Вот по времени последнего обращения можно оценить: оставлять такую запись или уже пора удалять.
Также эту таблицу можно использовать для получения сведений о том, кто в данный момент работает с приложением
Смотрите также
Как вывести приложение на передний план
Заметки на сайте Юрия Шутенко
Kernel Object Namespaces