Взаимодействие экземпляров приложения — Архив WASM.RU

Все статьи

Взаимодействие экземпляров приложения — Архив WASM.RU

  Вспоминается одна смешная история. Во времена Word 6.0, работая как-то в одной солидной организации, трепетно относившейся к своим коммерческим тайнам, был приглашен возмущенным соседом по комнате к его компьютеру. На экране красовалось нечто вроде: "Вы не можете открыть этот очень конфиденциальный документ, так как его уже открыл пользователь имярек". Имяреком был назван начальник отдела эксплуатации, сидевший в другом конце здания. Параноидальное сознание сразу стало выстраивать сложную схему шпионажа через локальную сеть, который развернул этот самый начальник. Другая часть мозга, пользуясь невытесняющей многозадачностью, принялась прикидывать размер вознаграждения за его поимку. Короче, помчались разбираться. Через пять минут половина отдела эксплуатации во главе с недоумевающим начальником толклась около компьютера и выдвигала идеи. В итоге оказалось, что никакого шпионажа нет, а ...

  ... а во всем виноваты Word 6.0, невнимательный пользователь и пиратство. Пиратство виновато потому, что так тогда делали все. Word 6.0 инсталлировался с дискет, которые добрый начальник отдела эксплуатации по очереди раздавал всем желающим, так что под его именем рожала документы вся организация. Невнимательный пользователь виноват потому, что редактировал-редактировал документ в одном экземпляре Word'а, а потом взял, да и попытался открыть его из файл-менеджера еще раз. А Word виноват потому, что уж так он тогда обеспечивал взаимодействие нескольких экземпляров приложения (сейчас гораздо лучше).

Больше всех жалко начальника отдела эксплуатации, которому пришлось прилично понервничать. Пользователь-то, которому его заложил глупый Word, был сотрудником отдела безопасности!

  Многозадачность Windows, являясь безусловным благом, не подлежащим ныне обсуждению и сомнению, является вместе с тем источником многих вызовов, бросаемых программисту. Некоторые из них весьма изощренны и ответ на них требует глубокого понимания принципов организации рабочей среды.

Например, при разработке приложения MyCall пришлось столкнуться с такой проблемой. Как известно, Remote Access Service (RAS), работающий в асинхронном режиме, запускается в отдельном потоке (нити), отличном от основного потока приложения. Также не менее хорошо известно, что для контроля процесса установления соединения в RAS используется callback-функция (например, RasDialFunc), которую работающий RAS вызывает при необходимости сообщить приложению о своем состоянии. Так вот, если у программиста имеется потребность передать из RasDialFunc какое-либо пользовательское сообщение окну приложения, то он может побояться воспользоваться для этой цели функцией SendMessage, полагая, что два потока, одновременно запускающие одну и ту же оконную процедуру в одном контексте - это пролог к катастрофе. Между тем такое действие вполне допустимо и безопасно, так как умная Windows, прежде чем передать управление оконной процедуре, проверяет, в каком потоке создавалось соответствующее окно, и при необходимости переключает потоки. При этом передающий поток замораживается на время обработки сообщения принимающим потоком.

  Другие же проблемы многозадачности более тривиальны, даже рутинны. Одна из них встает всякий раз, когда программист начинает работу над новым проектом: что делать в случае попытки запуска приложения, если оно уже запущено?

  В самом деле. Сама по себе Windows не ограничивает возможности пользователя запускать какое-либо приложение любое разумное количество раз. Выполнив два двойных щелчка по одной и той же иконке, пользователь может получить два экземпляра одного и того же приложения, и каждый из них будет функционировать в собственном процессе и собственном контексте так, как будто другого не существует.

Правда, только до той поры, пока они не попытаются разделить между собой какой-нибудь ресурс (в широком смысле), который разделять невозможно или нельзя. Например, невозможно разделять коммуникационный порт. И нельзя разделять общий файл данных, если оба экземпляра приложения пишут в него противоречащие друг другу данные. Разнообразие возникающих в этом случае эффектов может порадовать самого взыскательного искателя развлечений.

  Принятие решения о том, как поступать со вторым и последующими экземплярами приложения, целиком возлагается на программиста. Можно придумать много разных вариантов стратегии поведения приложения, в зависимости от назначения приложения. Среди них такие:

  1. Второй и последующие экземпляры приложения запускаются и функционируют самостоятельно, безотносительно к существованию друг друга
  2. Запуск второго экземпляра (и, следовательно, всех последующих) запрещен, пока функционирует первый запущенный экземпляр
  3. При попытке запуска второго экземпляра он не запускается, а в работе первого происходят некоторые изменения
Яркий пример третьей стратегии реализован в Word. Если экземпляр приложения уже запущен, и пользователь выполняет двойной щелчок по какому-нибудь файлу .doc в Проводнике, то запускаемый при этом второй экземпляр Word передает реквизиты загружаемого файла первому экземпляру и завершается, а первый экземпляр загружает файл в новое окно документа.

  С первой стратегией все просто. При запуске приложения ничего делать не надо - и тогда вы сможете наблюдать на экране столько экземпляров вашего приложения, сколько нужно, чтобы получить моральное удовлетворение. Главное - позаботиться, чтобы экземпляры приложения никогда не пытались разделять ресурсы, которые невозможно или нельзя разделять.

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

Поговаривают, что не всегда все было так трагично. Во времена наших предков, когда "Докторская" стоила 2.20, а Билл был еще совсем бедным со своими двумя десятками гигабаксов, заработанных на DOSе, это делалось очень просто. В win16 имел смысл параметр hPrevInstance функции WinMain. Достаточно было убедиться, что он ненулевой - и приложение знало, что оно не одиноко на этом свете. Но когда разрабатывалась win32, кто-то из главных системщиков Microsoft лопухнулся, а потом, когда все всплыло, на специальном совещании было принято решение прикрыть провинившегося и объяснить все произошедшее какими-нибудь благообразными словами, вроде: "Мы так и хотели с самого начала, а hPrevInstance сохранили для совместимости снизу вверх." Так или иначе - но hPrevInstance теперь бесполезен. А следует пользоваться средствами межпроцессного взаимодействия (Interprocess Communications), которые, к чести Microsoft, реализованы в Windows великолепно.

  Способов дать знать второму экземпляру о существовании первого можно придумать много. Один из них прямо дается в MSDN в статье WinMain:

hPrevInstance
Identifies the previous instance of the application. For a Win32-based application, this parameter is always NULL. If you need to detect whether another instance already exists, create a named mutex using the CreateMutex function. If the GetLastError function returns ERROR_ALREADY_EXISTS, another instance of your application exists (it created the mutex).

Идентифицирует предыдущий экземпляр приложения. Для Win32-приложений этот параметр всегда равен NULL. Если вам необходимо определить, существует ли уже экземпляр приложения, создайте именованный объект mutex, используя функцию CreateMutex function. Если функция GetLastError вернет ERROR_ALREADY_EXISTS, другой экземпляр вашего приложения существует (так как он уже создал mutex).

  Вот этой самой идеей и воспользуемся, слегка модифицировав ее:

.386
.Model flat,stdcall

;///////////////////////////////////////////////////////////// windows.inc
OpenMutexA        PROTO :DWORD,:DWORD,:DWORD      ;прототипы функций API
CreateMutexA      PROTO :DWORD,:DWORD,:DWORD
ExitProcess       PROTO :DWORD

SYNCHRONIZE              =00100000h               ;константы типов доступа
STANDARD_RIGHTS_REQUIRED =000f0000h
MUTANT_QUERY_STATE       =0001h
MUTANT_ALL_ACCESS        =STANDARD_RIGHTS_REQUIRED OR SYNCHRONIZE 
                          OR MUTANT_QUERY_STATE
MUTEX_ALL_ACCESS         =MUTANT_ALL_ACCESS

NULL                     =0                       ;разные константы
FALSE                    =0
TRUE                     =1

;///////////////////////////////////////////////////////////// WinMain
EXIT_OK                    =0                              ;коды выхода
EXIT_ALREADY_EXIST         =1
.const
  check_inst_mutex_name    db "Check Instant Mutex 0001",0 ;имя mutex
.data?
  check_inst_mutex         dd ?                            ;дескриптор mutex
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show
;................................................. проверка повторного запуска
 mov check_inst_mutex,0
 invoke OpenMutexA,MUTEX_ALL_ACCESS,FALSE,offset check_inst_mutex_name
 .if(!eax)    ;если mutex не существует - создать его и запустить приложение
  invoke CreateMutexA,NULL,TRUE,offset(check_inst_mutex_name)
  mov check_inst_mutex,eax
 .else        ;если mutex существует - прервать работу приложения
  invoke ExitProcess,EXIT_ALREADY_EXIST
 .endif
;......................................................... тело приложения
;...
;...                      ;весь остальной текст приложения
;...
;......................................................... завершение работы
 .if(check_inst_mutex!=0)                     ;уничтожить объект mutex
  invoke ReleaseMutex,check_inst_mutex
 .endif
 invoke ExitProcess,EXIT_OK
WinMain ENDP
;#############################################################
end

  Как видим, логика работы очень проста. Первым делом пытаемся открыть mutex с заданным именем.

Если вы - автор многих разных приложений, имейте в виду, что одинаковое имя mutex'а позволит запускаться только одному из этих приложений. Если ваше чувство юмора не столь развито, не забывайте модифицировать имя mutex'а в разных приложениях.

  Если mutex открыть не удается, значит запускаемый нами экземпляр приложения уникален, и мы смело можем продолжат его работу. Только перед этим создадим mutex, чтобы не дать запускаться последующим экземплярам.

  Ну и, конечно, при завершении работы приложения следует не забыть вызвать функцию ReleaseMutex.

В принципе, это не обязательно, так как если не освободить mutex с помощью этой функции, по завершении работы приложения он переводится в состояние WAIT_ABANDONED, которое в данном случае не мешает достижению стоящей перед нами задачи. Однако хороший стиль программирования предполагает явное освобождение занимаемых приложением ресурсов.
А вот Сергей AKA The Byte Reaper (http://neptunix.narod.ru) совершенно справедливо обратил наше внимане на то, что приведенный выше пример реализации второй стратегии сильно перегружен формальностями и реверансами в сторону MSDN. Полностью сохраняет ту же функциональность, но гораздо приятнее взору настоящего ассемблерщика предложенный Сергеем вот какой вариант:
;=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
invoke CreateMutexA,NULL,0,offset(check_inst_mutex_name)
invoke GetLastError
.if(eax==ERROR_ALREADY_EXISTS)
  invoke ExitProcess,EXIT_ALREADY_EXIST
.endif
;=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Идея проста предельно. Если создаваемый mutex уже присутствует в системе, то функция GetLastError вовращает код ERROR_ALREADY_EXISTS. Кстати, функция CreateMutex возвращает при этом вполне валидный дескриптор существующего mutex'а, но нас это совершенно не должно интересовать. Мы просто заканчиваем работу приложения - и все.

  Третья стратегия существенно сложнее в реализации, а гамма возможных решений намного больше. Очевидно, ее основой должно быть не только взаимное оповещение экземплярами приложения о существовании друг друга, но и обмен между ними какой-то информацией. Все зависит от того, какой объем информации потребуется. Если это одно-два двойных слова, то может оказаться достаточным использовать систему сообщений. В других случаях имеет смысл, например, организовать разделяемую память посредством File Mapping.

  Пример реализации третьей стратегии, использующий только систему оконных сообщений, можно найти в приложении MyCall. Там используется регистрируемое глобальное сообщение, которое приложение передает в широковещательном режиме в надежде, что уже запущенный экземпляр ответит ему тем же сообщением. Если такой ответ получен, то делается вывод, что один экземпляр приложения уже запущен, и запускаемый экземпляр завершает работу. Но перед этим он извлекает из ответного сообщения дескриптор главного окна работающего экземпляра и выводит это окно на первый план, чтобы удовлетворить потребность пользователя в доступе к приложению.

  Здесь мы приведем версию, выполняющую ту же задачу, но несколько отличную в реализации. По сути, она является гибридом из описанной выше и примененной в MyCall: для определения факта повторного запуска приложения используется mutex, а для передачи дескриптора окна работающего приложения вновь запускаемому - глобальное сообщение.

  Это полностью работающее приложение. В его основу положен шаблон оконного приложения. Содержимое файла windows.inc не приводится (то, что он должен собой представлять, показано в предыдущем примере. Недостающие элементы следует добавить самостоятельно, по аналогии, пользуясь заголовочными файлами для C/C++).

.386
.Model flat,stdcall

include windows.inc                     ;заголовочный файл API

;///////////////////////////////////////////////////////////// WinMain
EXIT_OK                   =0            ;коды выхода
EXIT_ERROR                =1
EXIT_ALREADY_EXIST        =2

QUESTION_PRIME_HWND       =1            ;запрос идентификатора окна
ANSWER_PRIME_HWND         =2            ;ответ идентификатора окна

.data?
  win_class               WNDCLASSEXA{} ;структура класса главного окна
  main_window             dd ?          ;дескриптор главного окна
  check_inst_mutex        dd ?          ;дескриптор mutex'а
  second_instance         dd ?          ;признак второго экземпляра
  check_inst_message      dd ?          ;идентификатор регистрируемого сообщения
  loop_message            MSG{}         ;сообщение для цикла обработки
.const
  app_name                db "Single Instance Application",0
  class_name              db "Single Instance Application Class",0
  check_inst_mutex_name   db "Check Instant Mutex 0001",0
  check_inst_message_name db "Check Instant Message 0001",0
.code
;:::::::::::::::::::::::::::::::::::::::::::::::::::::::::
WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show
;................................ регистрация класса окна
  mov win_class.cbSize,sizeof(WNDCLASSEXA)
  mov win_class.style,CS_HREDRAW OR CS_VREDRAW
  mov win_class.lpfnWndProc,offset win_procedure
  mov win_class.cbClsExtra,0
  mov win_class.cbWndExtra,0
  invoke GetModuleHandleA,NULL
  mov win_class.hInstance,eax
  invoke LoadIconA,NULL,IDI_APPLICATION
  mov win_class.hIcon,eax
  mov win_class.hIconSm,eax
  invoke LoadCursorA,NULL,IDC_ARROW
  mov win_class.hCursor,NULL
  mov win_class.hbrBackground,COLOR_WINDOWFRAME
  mov win_class.lpszMenuName,NULL
  mov win_class.lpszClassName,offset class_name
  invoke RegisterClassExA,offset win_class
  .if(!eax)
    jmp abort
  .endif
;....................... .......... создание окна
  invoke CreateWindowExA,NULL,\
                         offset class_name,\
                         offset app_name,\
                         WS_OVERLAPPEDWINDOW,\
                         100,100,300,200,\
                         NULL,\
                         NULL,\
                         win_class.hInstance,\
                         NULL
  .if(!eax)
    jmp abort
  .endif
  mov main_window,eax
;................................. зарегистрировать глобальное сообщение
 mov check_inst_message,0
 invoke RegisterWindowMessageA,offset check_inst_message_name
 .if(eax)
  mov check_inst_message,eax
 .endif
;.................................. проверить повторный запуск
 mov check_inst_mutex,0
 invoke OpenMutexA,MUTEX_ALL_ACCESS,FALSE,offset check_inst_mutex_name
 .if(!eax)
  mov second_instance,FALSE
  invoke CreateMutexA,NULL,TRUE,offset check_inst_mutex_name
  mov check_inst_mutex,eax
 .else
  mov second_instance,TRUE
  .if(check_inst_message)
   invoke SendMessageA,HWND_BROADCAST,check_inst_message,
                       QUESTION_PRIME_HWND,NULL

 .endif
  invoke ExitProcess,EXIT_ALREADY_EXIST
 .endif
;.................................. вывод окна на экран
  invoke ShowWindow,main_window,SW_SHOW
;................................... инициализация
;здесь можно вставить все необходимые действия для первичной инициализации
;приложения (например, загрузку и разбор файла настроек)
;......................................... цикл опроса очереди сообщений
msg_loop:
  invoke GetMessageA,offset loop_message,NULL,0,0
  .if(!eax)                                         ;завершение работы
   .if(check_inst_mutex!=0)                           ;освободить mutex
    invoke ReleaseMutex,check_inst_mutex
   .endif
   invoke ExitProcess,loop_message.wParam
  .endif
  invoke TranslateMessage,offset loop_message
  invoke DispatchMessageA,offset loop_message
  jmp msg_loop
abort:
  invoke ExitProcess,EXIT_ERROR
WinMain ENDP

;/////////////////////////////////////////////////////////// Оконная процедура
win_procedure PROC hwnd,message,w_param,l_param
  mov eax,message
;................................. обработка регистрируемого сообщения
 .if(eax==check_inst_message)   ;если получено зарегистрированное сообщение
  .if(check_inst_message)
   .if(second_instance)            ;если это второй экземпляр
    .if(w_param==ANSWER_PRIME_HWND)    ;если в сообщении получен дескриптор окна
     invoke SetForegroundWindow,l_param    ;вывести это окно на первый план
     jmp worked
    .endif
   .else                              ;если это первый экземпляр
    .if(w_param==QUESTION_PRIME_HWND)   ;если запрашивается дескриптор окна
     mov eax,hwnd                        ;послать ему дескриптор главного окна
     .if(eax==main_window)
      invoke SendMessageA,HWND_BROADCAST,check_inst_message,
	                      ANSWER_PRIME_HWND,main_window
      jmp worked
     .endif
    .endif
   .endif
  .endif
  jmp noworked
;............................................ закрытие приложения
 .elseif(eax==WM_DESTROY)
  invoke PostQuitMessage,EXIT_OK
  jmp worked
;............................................ обработка остальных сообщений
; .elseif(message==...)
;   ...
; .elseif(message==...)
;   ...
; .elseif(message==...)
;   ...
;.............................. ............. сообщение не обработано
 .else
noworked:
  invoke DefWindowProcA,hwnd,message,w_param,l_param
  ret
;............................................ сообщение обработано
 .endif
worked:
 xor eax,eax
 ret
win_procedure ENDP
;#############################################################
end

  Общая идея такова:

  • регистрируется класс главного окна и создается само окно с тем, чтобы вновь запускаемое приложение могло принимать и обрабатывать сообщения с помощью оконной процедуры win_procedure
  • регистрируется глобальное сообщение, которое впоследствии должно использоваться для обмена информацией между экземплярами приложения
  • выполняется проверка наличия уже работающего экземпляра приложения с помощью объекта mutex
  • если уже работающий экземпляр приложения существует - ему посылается инициирующее сообщение с параметром QUESTION_PRIME_HWND. Поскольку дескриптор окна этого экземпляра неизвестен, сообщение посылается широковещательно (HWND_BROADCAST).
    • ответ от уже работающего экземпляра принимает оконная процедура приложения в виде такого же сообщения, но уже с параметром ANSWER_PRIME_HWND. При этом второй параметр сообщения представляет собой дескриптор окна работающего приложения.
    • запускаемый второй экземпляр приложения, пользуясь полученным дескриптором, выводит окно первого экземпляра на первый план с помощью функции SetForegroundWindow и завершает свою работу
  • если запускаемый экземпляр приложения - первый и единственный, то он продолжает свою работу так, как задумал программист. При этом его оконная процедура всегда готова получить и обработать зарегистрированное сообщение с параметром QUESTION_PRIME_HWND, которое могут послать повторно запускаемые экземпляры. В ответ на такое сообщение работающий экземпляр отвечает таким же сообщением, но с параметром ANSWER_PRIME_HWND, передавая в качестве второго параметра дескриптор своего главного окна.

  См. также интересный, эффективный и простой вариант решения рассматриваемой задачи, предложенный Геннадием Майко.

2002-2013 (c) wasm.ru