Буквально на неделе на форуме появились два интересных вопроса, ответ на которые был очевиден, но... Программист, как вы знаете, существо с очень пытливым мозгом, он любит различные эксперименты, не смотря на то, что ответ на задачу мог быть уже озвучен :)
Впрочем, давайте посмотрим на первый вопрос:
Запускаю в отдельном потоке некий процесс (не мой, переделывать его не имею возможности), который необходимо завершить вместе с завершением основной (моей) программы.Если моя программа завершается штатно - то ничего сложного нет. Но если не штатно (пользователь убил через диспетчер задач) - так как быть тут?Попробуем подсказать...
В голову пока приходит только CreateRemoteThread+LoadLibrary+моя dll, которая будет следить за основным процессом.Подскажите более изящные решения.
Конечно же, это называется планирование, и самое первое, что приходит на ум - создать JOB, в который подключить дочерний процесс посредством вызова AssignProcessToJobObject.
Что есть JOB - по сути это достаточно удобный механизм позволяющий управлять множеством процессов, которые включены в него. У него есть много интересных фишек, но нас интересует только одна - надо закрыть все принадлежащие ему процессы как только JOB будет завершен.
Делается это достаточно просто:
У структуры TJobObjectExtendedLimitInformation, передаваемой на вход функции SetInformationJobObject, выставляем флаг JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, после чего все дочерние процессы запускаем с флагом CREATE_BREAKAWAY_FROM_JOB при вызове CreateProcess.
Как-то не понятно?
Тогда по шагам:
- создаем JOB вызовом CreateJobObject
- выставляем флаг закрытия всех дочерних процессов вызовом SetInformationJobObject
- запускаем дочерние процессы через CreateProcess
- каждый дочерний процесс подсоединяем к JOB вызовом AssignProcessToJobObject
- работаем как обычно
Но самое главное - если рутовый процесс держащий хэндл JOB-а прибьют через диспетчер задач, JOB все равно завершится, что и требовалось автору вопроса.
Впрочем, пожалуй хватит балаболить, давайте посмотрим на код.
В качестве дочернего процесса я выбрал штатный калькулятор, основное приложение будет запускать 4 его копии и, при завершении самого себя, будет их прибивать.
Для начала пишем обвес:
unit job_api; interface // непакованные структуры должны быть выровнены правильно, поэтому включаем директиву {$Align 8} uses Windows; const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = $00002000; JOB_OBJECT_ASSIGN_PROCESS = 1; CREATE_BREAKAWAY_FROM_JOB = $01000000; type PSecurityAttributes = ^TSecurityAttributes; _SECURITY_ATTRIBUTES = record nLength: DWORD; lpSecurityDescriptor: Pointer; bInheritHandle: BOOL; end; TSecurityAttributes = _SECURITY_ATTRIBUTES; PJobObjectBasicLimitInformation = ^TJobObjectBasicLimitInformation; TJobObjectBasicLimitInformation = packed record PerProcessUserTimeLimit: TLargeInteger; PerJobUserTimeLimit: TLargeInteger; LimitFlags, MinimumWorkingSetSize, MaximumWorkingSetSize, ActiveProcessLimit, Affinity, PriorityClass, SchedulingClass: DWORD; end; TIOCounters = record ReadOperationCount, WriteOperationCount, OtherOperationCount, ReadTransferCount, WriteTransferCount, OtherTransferCount: Int64; end; PJobObjectExtendedLimitInformation = ^TJobObjectExtendedLimitInformation; TJobObjectExtendedLimitInformation = record BasicLimitInformation: TJobObjectBasicLimitInformation; IoInfo: TIOCounters; ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed: DWORD; end; TJobObjectInfoClass = ( JobObjectBasicLimitInformation = 2, JobObjectBasicUIRestrictions = 4, JobObjectSecurityLimitInformation = 5, JobObjectEndOfJobTimeInformation = 6, JobObjectAssociateCompletionPortInformation = 7, JobObjectExtendedLimitInformation = 9, JobObjectGroupInformation = 11, JobObjectNotificationLimitInformation = 12, JobObjectGroupInformationEx = 14, JobObjectCpuRateControlInformation = 15 ); function CreateJobObjectA(lpJobAttributes: PSecurityAttributes; lpName: LPCSTR): THandle; stdcall; external kernel32 name 'CreateJobObjectA'; function SetInformationJobObject(hJob: THandle; JobObjectInformationClass: TJobObjectInfoClass; lpJobObjectInformation: Pointer; cbJobObjectInformationLength: DWORD): BOOL; stdCall; external kernel32 Name 'SetInformationJobObject'; function AssignProcessToJobObject(hJob, hProcess: THandle): BOOL; stdcall; external kernel32 name 'AssignProcessToJobObject'; function OpenJobObject(dwDesiredAccess: DWORD; bInheritHandle: BOOL; lpName: LPCTSTR): THandle; stdcall; external kernel32 name 'OpenJobObjectA'; procedure RunCalcAndAttachToJob(hJob: THandle); implementation uses SysUtils; procedure RunCalcAndAttachToJob(hJob: THandle); var SI: TStartupInfo; PI: TProcessInformation; SystemDir: AnsiString; begin ZeroMemory(@SI, SizeOf(TStartupInfo)); SI.cb := SizeOf(TStartupInfo); SetLength(SystemDir, MAX_PATH); GetSystemDirectoryA(@SystemDir[1], MAX_PATH); SystemDir := IncludeTrailingPathDelimiter(PAnsiChar(@SystemDir[1])) + 'calc.exe'; CreateProcessA(PChar(SystemDir), nil, nil, nil, True, CREATE_BREAKAWAY_FROM_JOB, nil, nil, SI, PI); CloseHandle(PI.hThread); AssignProcessToJobObject(hJob, PI.hProcess); end; end.
Тут просто декларация структур и API для работы с JOB, плюс процедура запуска калькулятора и аттача его к созданному ранее JOB-у RunCalcAndAttachToJob.
Пока не сильно вникайте в этот код, я на нем остановлюсь ниже.
А теперь посмотрим как будет выглядеть основное приложение:
program main_app1; uses Windows, SysUtils, job_api in '..\common\job_api.pas'; const JobName = 'MyJobName1'; var hJob: THandle; Limit: TJobObjectExtendedLimitInformation; begin hJob := CreateJobObjectA(nil, @JobName[1]); try if hJob = 0 then RaiseLastOSError; ZeroMemory(@Limit, SizeOf(TJobObjectExtendedLimitInformation)); Limit.BasicLimitInformation.LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, @Limit, SizeOf(TJobObjectExtendedLimitInformation)); RunCalcAndAttachToJob(hJob); RunCalcAndAttachToJob(hJob); RunCalcAndAttachToJob(hJob); RunCalcAndAttachToJob(hJob); MessageBox(0, 'После закрытия окна все дочерние процессы будут завершены.', nil, 0); finally CloseHandle(hJob); end; end.Ага - это все.
Просто создаем JOB и подключаем к нему процессы калькуляторов.
Просто?
Конечно.
А знаете как эта задача была решена в итоге?
Через создание удаленного потока, и не спрашивайте у меня: "почему это было сделано именно через него", не я автор вопроса и не я решал эту задачу :)
Впрочем...
Вопрос номер 2:
Есть программа, которая должна работать в единственном экземпляре и только пока работает хотя бы одна интересующая ее программа.Пробуем решить эту задачу через тот-же JOB.
Например, есть программы:A.exeB.exeC.exeиSupport.exe
A, B и C при запуске запускают Support.exe. Про этом Support.exe должен работать в единственном экземпляре (это легко сделать).Но как только закроются все экземпляры A, B и C - то Support тоже должен закрыться. (вот это не совсем понятно как сделать просто).При этом не надо полагаться на то, что A, B или C закроются корректно.А также, если пользователь прибьет Support в диспетчере задач, то это его личное дело.
Пока самая простая идея такая:Создать/Открыть именованный объект ядра во всех A, B и C.А в Support в потоке с интервалом проверять существует ли этот именованный объект.
Здесь задача немного усложняется, т.к. рутовых процессов может быть много, а дочерний - (support.exe) только один.
Решаем ее по стандартной схеме через наименование, а именно - у функции CreateJobObject вторым параметром идет имя задачи.
Зная это имя, второй процесс может открыть эту задачу и подключить самого себя к JOB-у посредством OpenJobObject и того-же AssignProcessToJobObject.
В качестве дочернего процесса (который изначально был support.exe) возьмем тот-же калькулятор и пишем логику по таким шагам:
- если при старте мы не можем открыть задачу по ее имени - значит мы первые
- если мы первые - создаем задачу и подключаем к ней самого себя и калькулятор
- если вдруг такая задача уже существует - просто открываем ее и подключаем самого себя
Нюанс - теперь у нас хэндлов на задачу много (при каждом старте рутового процесса добавляется новый) но закрывать процессы мы можем в произвольном порядке и калькулятор все равно будет жить.
Смотрим код:
program main_app2; uses Windows, SysUtils, job_api in '..\common\job_api.pas'; const JobName = 'MyJobName2'; procedure WaitStop; begin MessageBox(0, PChar('Закройте окно дла завершения процесса: ' + IntToStr(GetCurrentProcessId)), nil, 0); end; function HaseJob: Boolean; var hJob: THandle; begin hJob := OpenJobObject(JOB_OBJECT_ASSIGN_PROCESS, False, PChar(@JobName[1])); Result := hJob <> 0; if Result then begin AssignProcessToJobObject(hJob, GetCurrentProcess); WaitStop; end; CloseHandle(hJob); end; procedure RunMainApp; var hJob: THandle; Limit: TJobObjectExtendedLimitInformation; begin hJob := CreateJobObjectA(nil, @JobName[1]); try if hJob = 0 then RaiseLastOSError; ZeroMemory(@Limit, SizeOf(TJobObjectExtendedLimitInformation)); Limit.BasicLimitInformation.LimitFlags := JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, @Limit, SizeOf(TJobObjectExtendedLimitInformation)); RunCalcAndAttachToJob(hJob); WaitStop; finally CloseHandle(hJob); end; end; begin if not HaseJob then RunMainApp; end.
Функция HaseJob проверяет наличие JOB и, будь таковой, подключает себя к нему.
Ну а если JOB не создан - создаем процесс калькулятор и аттачим его к JOB-у.
Проверьте - приложение main_app2 можно запустить сколько угодно раз, но калькулятор будет запущен только единожды и закроется только тогда, когда вы закроете самый последний экземпляр main_app2.
Немного комментариев по коду
Обратите внимание на выравнивание структур в модуле job_api.
Если структуры TIOCounters или TJobObjectExtendedLimitInformation будут неверно размещены в памяти - SetInformationJobObject выполнится не успешно.
Забудете флаг CREATE_BREAKAWAY_FROM_JOB - все это не заработает (см. MSDN).
Есть нюанс с консольками: если вы консоль и дочерние процессы тоже консоль - при закрытии консоли убьете всю кучу процессов. Надо как-то распределять дочерние процессы, к примеру через детач.
Писал все за вечер на Delphi7 и не проверял на юникодных вариантах Delphi (хард недавно умер - лень все переустанавливать), поэтому под юникодом не ручаюсь за правильность исполнения, но навскидку вроде ничего не пропустил и должно работать.
Исходный код можно забрать тут.
---
© Александр (Rouse_) Багель
Февраль, 2015
Спасибо. Интересно и полезно.
ОтветитьУдалитьОчень познавательно. Спасибо.
ОтветитьУдалитьТут вот что интересно, в связи с этой темой... К сожалению, в дочернем приложении запускаемом как JOB, похоже невозможно получить управление, если это приложение закрывается при снятии основного (например, через диспетчер задач). По крайней мере эксперименты и изучение MSDN такой способ не выявили.
ОтветитьУдалитьЕсли Вам известно как это сделать - был бы крайне признателен, если бы Вы его обозначили.
В двух словах, зачем это нужно. Основное приложение работает с БД Oracle. Часть работы (например, формирование отчёта) выполняется в дочернем приложении, запускаемом из основного как JOB. Дочернее приложение для работы с данными в согласованном состоянии в начале своей работы стартует транзакцию в режиме READ_ONLY.
Так вот, при снятии основного приложения, дочерний JOB будет закрыт, а транзакция, объявленная на сервере БД останется "висеть", что очень нехорошо. Если бы в дочернем приложении был способ получить управление при закрытии основного - можно было бы эту транзакцию корректно завершить перед завершением работы.
Более подробно есть у Рихтера: http://www.e-reading.link/bookreader.php/135055/Windows_dlya_professionalov:_sozdanie_effektivnyh_Win32-prilozheniii_s_uchetom_specifiki_64-razryadnoii....pdf
Удалитьстраница 112 (Уведомление заданий).
Большое спасибо Александр! :-)
УдалитьДействительно, без книжки догадаться о необходимости создания "порта завершения ввода-вывода" несколько нетривиально.
Попробуем - действительно интересно, что получится...
Блин... А я велосипед изобретал 😁..
ОтветитьУдалить