Буквально на неделе на форуме появились два интересных вопроса, ответ на которые был очевиден, но... Программист, как вы знаете, существо с очень пытливым мозгом, он любит различные эксперименты, не смотря на то, что ответ на задачу мог быть уже озвучен :)
Впрочем, давайте посмотрим на первый вопрос:
Запускаю в отдельном потоке некий процесс (не мой, переделывать его не имею возможности), который необходимо завершить вместе с завершением основной (моей) программы.Если моя программа завершается штатно - то ничего сложного нет. Но если не штатно (пользователь убил через диспетчер задач) - так как быть тут?Попробуем подсказать...
В голову пока приходит только 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 (Уведомление заданий).
Большое спасибо Александр! :-)
УдалитьДействительно, без книжки догадаться о необходимости создания "порта завершения ввода-вывода" несколько нетривиально.
Попробуем - действительно интересно, что получится...
Блин... А я велосипед изобретал 😁..
ОтветитьУдалить