Blog. Just Blog

Запуск ограниченного количества копий программы

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
В комментариях к статье о запрете запуска нескольких копий программы пару лет назад был задан очень правильный вопрос: как реализовать запуск ограниченного числа копий программы больше одного? Сейчас я могу ответить на этот вопрос полноценной статьей. Настоятельно рекомендую сперва прочитать изначальную статью, чтобы не дублировать здесь теоретические выкладки. Для наглядности почти все методы остаются прежними, меняться будут только условия их использования.

Первый способ использует shared-память, общую для всех копий данного приложения. В ней находится счетчик, который каждая копия увеличивает на 1 при запуске и уменьшает на 1 при завершении своей работы. При старте выполняется проверка на количество ранее запущенных копий. Счетчик достиг максимального значения - пожалуйте на выход.
  1. ; Расшаренная секция, общая для всех копий данного приложения
  2. section '.shared' data readable writeable shareable
  3. started  dd  0       ; Количество запущенных копий
  4.  
  5. ; Сегмент кода
  6. section '.code' code readable executable 
  7.         cmp     [started],3     ; Уже запущено три экземпляра программы?
  8.         jae     already_started ; Да, на выход
  9.  
  10.         ; +1 к счетчику копий
  11.         ; Префикс LOCK используется для предотвращения возможных конфликтов
  12.         ; на многопроцессорных машинах
  13.         lock add [started],1
  14.  
  15.         ; Нормальный запуск программы 
  16.         ... 
  17.         ...
  18.         ...
  19.         ; -1 от счетчика копий
  20.         lock sub [started],1
  21.         jmp     loc_exit
  22.  
  23. already_started:
  24.         ; Выход из программы
  25.         ...
  26. loc_exit:
Обратите внимание на использование префикса LOCK перед командами изменения счетчика копий. Этот префикс предназначен для обеспечения исключительного доступа к ячейке памяти на время выполнения команды при работе на многопроцессорных многозадачных системах. В отличие от проверки единственной копии, у такого варианта решения есть большой недостаток. Если приложение завершится аварийно или будет принудительно завершено системой, то счетчик работающих копий не уменьшится. Обойти это можно, если, например, в общей памяти хранить не просто счетчик, а массив идентификаторов процессов, при каждом запуске проверяя их наличие и актуальность.

Второй вариант с поиском окон претерпел наибольшие изменения. Поскольку нам надо найти не одно окно, а посчитать количество окон, удовлетворяющих определенным условиям, то придется перебрать их все. Для этого используется функция EnumWindows.
  1. ; Сегмент данных
  2. section '.data' data readable writeable
  3.  
  4. title db 'Only Three Demo #2',0    ; Заголовок окна
  5.  
  6. buff  rb 100h
  7. count dd ? ; Счетчик найденных окон
  8.  
  9. ;---------------------------------------------
  10.  
  11. section '.code' code readable executable
  12.  
  13.   start:
  14.         ; Посчитать количество окон с нужным заголовком
  15.         mov     [count],0
  16.         invoke  EnumWindows,EnumFunc,0
  17.         ; Уже запущено максимальное количество копий?
  18.         cmp     [count],3
  19.         jae     already_started    ; Да, на выход
  20.         ; Нормальный запуск программы 
  21.         ... 
  22. already_started:
  23.         ; Выход из программы
  24.         ...
В callback-функции проверяем совпадение заголовка каждого окна с нужной нам строкой. В случае совпадения увеличиваем счетчик.
  1. proc EnumFunc hwnd:DWORD, lParam:DWORD
  2.         ; Получить заголовок окна
  3.         invoke  GetWindowText,[hwnd],buff,100h
  4.         ; Сравнить с нужным
  5.         invoke  lstrcmp,buff,title
  6.         or      eax,eax
  7.         jnz     @f
  8.         ; +1 к счетчику копий
  9.         inc     [count]
  10. @@:
  11.         mov     eax,TRUE
  12.         ret
  13. endp
Можно немного оптимизировать код, чтобы перебор окон прекращался при достижении критического значения счетчика. Зачем лишний раз нагружать систему, если результат уже известен и от дальнейших проверок не зависит.

Третий вариант также основан на мьютексах. Хотя казалось бы, мьютекс - уникальная сущность, он может быть в системе только в единственном экземпляре. Действительно, это так. Но нам никто не запрещает вести счетчик копий прямо в имени мьютекса. При старте программы поочередно перебираем мьютексы, если очередной мьютекс свободен, то занимаем его, иначе увеличиваем счетчик в имени. Если достигли максимального значения счетчика, то значит все свободные имена уже заняты работающими копиями приложения.
  1. ; Сегмент данных
  2. section '.data' data readable writeable
  3.  
  4. mutex db 'Only Three Demo #3_' ; Уникальное имя мьютекса
  5. count db '1'
  6.       db 0
  7.  
  8. hMutex dd ?
  9.  
  10. ;---------------------------------------------
  11.  
  12. section '.code' code readable executable
  13.  
  14. loc_mutex:
  15.         invoke  CreateMutex,NULL,TRUE,mutex ; Создать мьютекс
  16.         mov     [hMutex],eax
  17.         invoke  GetLastError             ; Проверить код ошибки
  18.         cmp     eax,ERROR_ALREADY_EXISTS ; Такой мьютекс уже есть?
  19.         jne     loc_ok
  20.         invoke  CloseHandle,[hMutex]
  21.         ; Увеличить счетчик в имени мьютекса
  22.         inc     byte [count]
  23.         ; Максимальное значение достигнуто?
  24.         cmp     byte [count],'4'
  25.         je      already_started          ; Да, на выход
  26.         jmp     loc_mutex
  27. loc_ok:
  28.         ; Нормальный запуск программы 
  29.         ... 
  30.         ... 
  31.         invoke  CloseHandle,[hMutex]
  32.         jmp     loc_exit
  33. already_started:
  34.         ; Выход из программы
  35.         ...
  36. loc_exit:
В приведенном примере счетчик в имени может быть максимум "9", но вам никто не мешает формировать имя мьютекса через wsprintf или еще как-нибудь.


Two or more processes can call CreateMutex to create the same named mutex. The first process actually creates the mutex, and subsequent processes open a handle to the existing mutex.


Это цитата из документации. Обратите внимание, что после попытки создания мьютекса, если он уже существует, обязательно вызывается команда CloseHandle, чтобы освободить открытый хэндл, хотя фактически сам хэндл нам не принадлежит. Эта же команда должна вызываться при завершении работы приложения.

Последний вариант использует виртуальные файлы, созданные функцией CreateFileMapping. Сперва у меня была мысль реализовать такой же трюк с уникальными именами, как в третьем примере с мьютексами, но потом я решил сделать что-нибудь более интересное. А именно хранить счетчик в этом виртуальном файле. Ну а что, память под него уже выделена, так хоть зря пропадать не будет. При первом запуске создается файл и в него записывается начальное значение счетчика, все последующие запуски и завершения работы будут просто менять это значение.
  1. ; Сегмент данных
  2. section '.data' data readable writeable
  3.  
  4. fname db 'Only Three Demo 4',0      ; Уникальное имя виртуального файла
  5. hMapping dd ?
  6. InstanceInfo  dd ?
  7.  
  8. ;---------------------------------------------
  9.  
  10. section '.code' code readable executable
  11.  
  12.   start:
  13.         ; Создать виртуальный файл
  14.         invoke  CreateFileMapping,0FFFFFFFFh,NULL,\
  15.                 PAGE_READWRITE,NULL,1024,fname
  16.         mov     [hMapping],eax
  17.  
  18.         invoke  GetLastError                   ; Проверить код ошибки
  19.         cmp     eax,ERROR_ALREADY_EXISTS       ; Такой файл уже есть?
  20.         je      @f
  21.  
  22.         ; Записать счетчик в файл
  23.         invoke  MapViewOfFile,[hMapping],FILE_MAP_ALL_ACCESS,0,0,4
  24.         mov     [InstanceInfo],eax
  25.         mov     dword [eax],1
  26.         jmp     loc_ok
  27. @@:
  28.         ; Открыть виртуальный файл
  29.         invoke  OpenFileMapping,FILE_MAP_ALL_ACCESS,FALSE,fname
  30.         mov     [hMapping],eax
  31.  
  32.         ; Прочитать счетчик из файла
  33.         invoke  MapViewOfFile,[hMapping],FILE_MAP_ALL_ACCESS,0,0,4
  34.         mov     [InstanceInfo],eax
  35.         cmp     dword [eax],3
  36.         jae     already_started                ; Да, на выход
  37.  
  38.         ; +1 к счетчику копий
  39.         inc     dword [eax]
  40. loc_ok:
  41.         ; Нормальный запуск программы 
  42.         ... 
  43.         ... 
  44.         ; -1 от счетчика копий
  45.         mov     eax,[InstanceInfo]
  46.         dec     dword [eax]
  47.         jmp     loc_exit
  48. already_started:
  49.         ; Выход из программы
  50.         ...
  51. loc_exit:
По принципу реализации этот вариант очень напоминает работу с shared-памятью, при этом обладает всеми его недостатками. При некорректном завершении процесс не сможет уменьшить счетчик копий. Обходится точно так же через хранение и проверку идентификаторов процессов.

В приложении примеры программ с исходными текстами для всех описанных способов. В каждом случае количество одновременно работающих копий ограничено тремя.

Примеры программ с исходными текстами (FASM)Примеры программ с исходными текстами (FASM)

There.Can.Be.Only.Three.Demo.zip (9,066 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 1355 | Комментариев: 3

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (31.10.2020 в 15:11):
Пример рисовать не хочу, лениво, поясню на словах. Вместо одного DWORD в расшаренной памяти будет, например, массив из трех двордов. При запуске программа находит элемент массива с нулевым значением, через GetCurrentProcessId получает идентификатор своего процесса, записывает его в пустую ячейку. При нормальном завершении работы находит ячейку со своим идентификатором и обнуляет ее. Если все три элемента массива уже имеют ненулевые значения, то запускаемая копия, например, через OpenProcess поочередно проверяет наличие каждого процесса с этими идентификаторами. Если все три работают - на выход. Если какого-то процесса нет, значит он завершился аварийно без очистки ячейки и можно перезаписывать ячейку с его идентификатором своими данными. Как-то так.
tshbg (31.10.2020 в 12:08):
ЦитатаОбойти это можно, если, например, в общей памяти хранить не просто счетчик, а массив идентификаторов процессов, при каждом запуске проверяя их наличие и актуальность.

Пример?
Andrey (05.06.2020 в 23:47):
Код я вам скажу очень красиво реализован

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.06 сек. / MySQL: 2 (0.0055 сек.) / Память: 4.5 Mb
Наверх