Предотвращение запуска более одного экземпляра приложения

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

Вопросы безопасности

  1. Важно гарантировать уникальность имени мьютекса или семафора. Для этого удобно использовать GUID.
  2. В POSIX необходимо тщательно задавать права доступа, чтобы избежать несанкционированного взаимодействия между процессами.
  3. Методы должны корректно обрабатывать ошибки. Например, если дескриптор мьютекса или семафора недействителен, программа должна возвращать ошибки или завершаться безопасным образом. Обратите внимание, что AppLock не везде учитывает это.