В некоторых ситуациях возникает необходимость предотвратить запуск более одного экземпляра приложения. Это может быть полезно для защиты от нежелательного поведения, конкуренции за ресурсы, нарушения логики работы и других подобных проблем. Для решения этой задачи можно использовать системные API Windows или POSIX.
Все нижеописанное корректно для любого языка программирования, умеющего взаимодействовать с API Windows и POSIX, а не только для D.
Мьютексы Windows
Для Windows используются механизмы Mutex
. Мьютексы — это объекты синхронизации, предоставляемые операционной системой, которые могут быть разделяемыми между процессами.
Создается мьютекс с помощью функции CreateMutex:
HANDLE CreateMutexW(LPSECURITY_ATTRIBUTES lpMutexAttributes, bool bInitialOwner, const wchar* lpName)
- lpMutexAttributes — ссылка на структуру с атрибутами безопасности для создаваемого мьютекса.
null
— означает, что мьютекс будет использовать атрибуты безопасности по умолчанию. Обычно это подходящий выбор, если доступ к мьютексу требуется только текущему пользователю. - bInitialOwner — указывает, должен ли вызывающий поток сразу захватить мьютекс. Использование
true
гарантирует, что после создания мьютекса другие потоки или процессы не смогут его захватить, пока текущий поток не освободит его. - lpName — имя мьютекса, идентифицирующее его в системе. Если требуется разделение доступа между несколькими процессами, необходимо передавать уникальное имя, например GUID.
Функция OpenMutex открывает существующий мьютекс:
HANDLE OpenMutexW(DWORD dwDesiredAccess, bool bInheritHandle, const wchar* lpName)
- dwDesiredAccess — указывает уровень доступа к мьютексу. Если приложение только проверяет состояние мьютекса, достаточно указать
SYNCHRONIZE
, иначе укажитеMUTEX_ALL_ACCESS
. - bInheritHandle — определяет, наследуется ли дескриптор мьютекса дочерними процессами. Если приложение запускает дочерний процесс, который также должен использовать мьютекс, необходимо передать
true
. - lpName — аналогичны параметру
lpName
изCreateMutex
.
Функция WaitForSingleObject
ожидает сигнала от объекта синхронизации:
DWORD WaitForSingleObject(HANDLE hMutex, DWORD dwMilliseconds)
- hMutex — дескриптор объекта синхронизации (мьютекса).
- dwMilliseconds — таймаут ожидания.
INFINITE
— бесконечное ожидание, пока объект не станет доступным. Если важно ограничить время ожидания, укажите конкретное значение в миллисекундах.
Функция ReleaseMutex освобождает мьютекс, позволяя другим потокам его захватить. В нашем случае используем при завершении процесса программы. Принимает дескриптор объекта синхронизации (мьютекса).
Таким образом, имеется ряд функций Windows API, который позволяет создавать, блокировать, открывать и освобождать мьютекс. Это все, что необходимо для реализации нашей задачи на Windows.
Именованные семафоры POSIX
POSIX-реализация использует именованные семафоры (sem_t
). Семафоры позволяют управлять доступом к разделяемым ресурсам.
Именованный семафор создается с помощью функции sem_open:
sem_t *sem_open(const char *name, int oflag, mode_t mode, uint value)
- name — имя семафора. Строка, начинающаяся с
/
. - oflag — режим открытия.
O_CREAT
— создаёт семафор, если он не существует.O_EXCL
— если семафор уже существует, возвращается ошибка. Обычно их комбинируют. - mode — права доступа.
S_IRUSR
— чтение владельцем,S_IWUSR
— запись владельцем. - value — начальное значение семафора. Укажите
1
для двоичного семафора (мьютекса).
sem_post и sem_wait обе принимают указатель на семафор:
sem_post
увеличивает значение семафора.sem_wait
уменьшает значение семафора, блокируя, если значение равно 0.
Функция sem_getvalue
получает текущее значение семафора:
int sem_getvalue(sem_t *sem, int *sval)
- sem — указатель на семафор.
- sval — указатель на переменную, в которую записывается значение семафора.
Функция sem_unlink удаляет семафор (по его имени), чтобы избежать “мусора” в системе после завершения работы приложения:
int sem_unlink(const char *name)
Это все, что необходимо для реализации нашей задачи на POSIX-системах.
Реализация AppLock
У нас имеется нужный набор функций Windows и POSIX, остается обернуть это в удобную утилиту. Мы используем для этого класс с рядом методов:
lock()
— создает и блокирует мьютекс или семафор;unlock()
— освобождает и удаляет мьютекс или семафор;isLocked()
— возвращает текущий статус мьютекса или семафора (занят или нет).
Полный код класса AppLock:
class AppLock { private string _guid; version (Windows) { import core.sys.windows.winbase; import core.sys.windows.windows; import core.sys.windows.winerror; this(string guid) { _guid = guid; } bool lock() { // Create the mutex HANDLE mutexHandle = CreateMutexW(null, true, cast(const wchar*) _guid.toStringz()); // Check if mutex already exists if (GetLastError() == ERROR_ALREADY_EXISTS) { return false; } else { // Lock the mutex WaitForSingleObject(mutexHandle, INFINITE); return true; } } bool unlock() { HANDLE hMutex = OpenMutexW(MUTEX_ALL_ACCESS, false, cast(const wchar*) _guid.toStringz()); if (hMutex != NULL) { if (ReleaseMutex(hMutex) == FALSE) return false; CloseHandle(hMutex); } return true; } bool isLocked() { HANDLE hMutex = OpenMutexW(MUTEX_ALL_ACCESS, 0, cast(const wchar*) _guid.toStringz()); if (hMutex != null) { DWORD res = WaitForSingleObject(hMutex, 0); CloseHandle(hMutex); if (res == WAIT_TIMEOUT) return true; } return false; } } version (Posix) { import core.sys.posix.sys.types; import core.sys.posix.semaphore; import core.sys.posix.fcntl; import core.sys.posix.unistd; import core.sys.posix.sys.stat; import core.sys.posix.sys.mman; private sem_t* _semaphore; private string _semName; this(string guid) { _guid = guid; _semName = "/" ~ guid; } bool lock() { _semaphore = sem_open(_semName.toStringz, O_CREAT | O_EXCL, S_IRUSR | S_IWUSR, 1); if (_semaphore == cast(sem_t*) SEM_FAILED) { _semaphore = sem_open(_semName.toStringz, 0); } if (_semaphore is null) { return false; } return sem_wait(_semaphore) == 0; } bool unlock() { if (_semaphore is null) { return false; } if (sem_post(_semaphore) != 0) { return false; } // Unlink the semaphore if (sem_unlink(_semName.toStringz) != 0) { return false; } return true; } bool isLocked() { if (_semaphore is null) { return false; } int sval; if (sem_getvalue(_semaphore, &sval) != 0) { return false; } return sval == 0; } } }
Пример использования
void main() { auto appLock = new AppLock("unique-app-id"); if (!appLock.lock()) { writeln("Another instance of the application is already running."); return; } writeln("Application is running. Press Enter to exit."); readln(); appLock.unlock(); }
Получилось универсальное решение для предотвращения запуска нескольких экземпляров приложения на различных платформах. Используя системные API, он обеспечивает эффективный механизм синхронизации.
Вопросы безопасности
- Важно гарантировать уникальность имени мьютекса или семафора. Для этого удобно использовать GUID.
- В POSIX необходимо тщательно задавать права доступа, чтобы избежать несанкционированного взаимодействия между процессами.
- Методы должны корректно обрабатывать ошибки. Например, если дескриптор мьютекса или семафора недействителен, программа должна возвращать ошибки или завершаться безопасным образом. Обратите внимание, что AppLock не везде учитывает это.