Тестовое приложение Process Viewer
Чтобы аккуратно завершить приложение, дав ему произвести необходимую очистку, мы должны послать сообщение WM_CLOSE всем видимым окнам верхнего уровня, принадлежащим приложению. Посылка сообщения WM_CLOSE эквивалентна команде Закрыть системного меню окна. Если приложение написано правильно, то оно освободит ресурсы и завершится. Мы должны быть готовы к тому, что процесс завершения может затянуться. Hапример, приложение может спросить у пользователя, хочет ли он сохранить измененные файлы. Если приложение не завершится за разумное время, нам не останется ничего, кроме как применить "тяжелую артиллерию" - функцию TerminateProcess.
Чтобы предоставить максимальную гибкость в организации ожидания, мы решили написать функцию остановки приложения в соответствии с приведенным ниже прототипом.
#define KILLAPP_TERMINATE -1 #define KILLAPP_WAIT 0 #define KILLAPP_CANCEL 1 typedef int (CALLBACK * PFNWAITCALLBACK)(LPARAM); BOOL KillApplication( IN HWND hWnd, // идентификатор окна IN PFNWAITCALLBACK pfnWaitCallback, // пользовательская функция IN LPARAM lParam // параметр для пользовательской функции ); |
Параметр hWnd идентифицирует приложение, которое мы хотим завершить, это может быть любое окно, принадлежащее приложению. Параметр pfnWaitCallback задает пользовательскую функцию, которая будет периодически вызываться в процессе ожидания. Программа может использовать эту фунцию для обработки сообщений, пока функция ожидает завершения останавливаемого процесса. Кроме того, пользовательская функция определяет, когда уже достаточно ждать и пора применить грубую силу: возвращаемое значение функции определяет дальнейшее поведение KillApplication:
KILLAPP_TERMINATE | Немедленно завершить процесс с помощью TerminateProcess |
KILLAPP_WAIT | Продолжить ожидание |
KILLAPP_CANCEL | Отменить операцию и вернуть управление, не дожидаясь завершения приложения |
Параметр lParam передается пользовательской функции без изменений.
Ниже приведена реализация функции KillApplication:
BOOL KillApplication( IN HWND hWnd, IN PFNWAITCALLBACK pfnWaitCallback, IN LPARAM lParam ) { _ASSERTE(pfnWaitCallback != NULL); if (!IsWindow(hWnd)) return SetLastError(ERROR_INVALID_PARAMETER), FALSE; // определяем идентификатор процесса и потока, которым // принадлежит указанное окно DWORD dwProcessId; DWORD dwThreadId = GetWindowThreadProcessId(hWnd, &dwProcessId); OSVERSIONINFO osvi; osvi.dwOSVersionInfoSize = sizeof(osvi); // определяем версию операционной системы GetVersionEx(&osvi); // проверяем, не является ли это приложение 16-битной задачей // на Windows NT BOOL b16bit = FALSE; HINSTANCE hVdmDbg = NULL; if (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT) { // загружаем VDMDBG.DLL hVdmDbg = LoadLibrary(_T("vdmdbg.dll")); if (hVdmDbg == NULL) return FALSE; b16bit = IsWOWProcess(hVdmDbg, dwProcessId); } if (!b16bit) { // открываем описатель процесса HANDLE hProcess = OpenProcess(SYNCHRONIZE|PROCESS_TERMINATE, FALSE, dwProcessId); if (hProcess == NULL) { DWORD dwError = GetLastError(); if (hVdmDbg != NULL) FreeLibrary(hVdmDbg); return SetLastError(dwError), FALSE; } // посылаем сообщение WM_CLOSE всем видимым окнам, принадлежащим // процессу EnumWindows(KillAppEnumWindows, dwProcessId); // основной цикл ожидания int nRet = KILLAPP_WAIT; while (WaitForSingleObject(hProcess, 100) == WAIT_TIMEOUT) { nRet = pfnWaitCallback(lParam); if (nRet != KILLAPP_WAIT) break; } // завершаем процесс, если пользовательская функция // решила поступить таким образом if (nRet == KILLAPP_TERMINATE) TerminateProcess(hProcess, (UINT)-1); CloseHandle(hProcess); } else { // посылаем сообщение WM_CLOSE всем окнам верхнего уровня, // принадлежащим тому же потоку EnumWindows(KillAppEnumWindows16, dwThreadId); // ожидаем, пока 16-битное приложение не завершился int nRet = KILLAPP_WAIT; WORD wTaskId; while ((wTaskId = IsWOWTask(hVdmDbg, dwProcessId, dwThreadId)) != 0) { Sleep(100); nRet = pfnWaitCallback(lParam); if (nRet != KILLAPP_WAIT) break; } // завершаем задачу принудительно if (nRet == KILLAPP_TERMINATE) TerminateWOWTask(hVdmDbg, dwProcessId, wTaskId); } if (hVdmDbg != NULL) FreeLibrary(hVdmDbg); return TRUE; } |
Сначала функция определяет идентификаторы процесса и потока, которым принадлежит указанное окно - они нам пригодятся в дальнейшем для идентификации останавливаемого приложения. Затем функция проверяет, не принадлежит ли указанное окно 16-битной задаче в Windows NT. 16-битные задачи требуют специальной обработки и мы рассмотрим их отдельно.
В 32-битном случае, функция открывает описатель (handle) процесса с правами доступа SYNCHRONIZE и PROCESS_TERMINATE. Если мы не имеем таких прав по отношению к завершаемому процессу, функция сразу же завершается с ошибкой. Затем функция рассылает сообщение WM_CLOSE всем окнам верхнего уровня, принадлежащим этому процессу, для чего мы используем EnumWindows, указав в качестве функции перечисления функцию KillAppEnumWindows, текст которой приведен ниже.
static BOOL CALLBACK KillAppEnumWindows( IN HWND hWnd, IN LPARAM lParam ) { _ASSERTE(lParam != 0); DWORD dwProcessId; GetWindowThreadProcessId(hWnd, &dwProcessId); if (IsWindowVisible(hWnd) && dwProcessId == (DWORD)lParam) PostMessage(hWnd, WM_CLOSE, 0, 0); return TRUE; } |
После того, как сообщения разосланы, функция переходит в цикл ожидания. Каждые 100 миллисекунд она вызывает пользовательскую функцию, давая ей возможность обработать накопившиеся сообщения и принять решение, стоит ли продолжать ожидание. Выход из цикла происходит, когда случится одно из двух событий: процесс завершится и WaitForSingleObject вернет WAIT_OBJECT_0, либо пользовательская функция вернет значение, отличное от KILLAPP_WAIT. Если возвращаемым значением пользовательской функции было KILLAPP_TERMINATE, то процесс завершается принудительно с помощью TerminateProcess.
Как уже отмечалось, 16-битные задачи обрабатываются особенным образом. Главное их отличие от 32-битных задач для нас состоит в том, что несколько 16-битных задач могут разделять один процесс WOW VDM, поэтому та логика, которую мы использовали для завершения 32-битных приложений является непригодной. Прежде всего, вот код функции IsWOWProcess, которая используется для того, чтобы отличить 16-битные задачи:
static BOOL CALLBACK EnumProcessWOWProc( IN DWORD dwProcessId, IN DWORD dwAttributes, IN LPARAM lParam ) { _ASSERTE(lParam != 0); if (dwProcessId == *(DWORD *)lParam) { *(DWORD *)lParam = 0; return TRUE; } return FALSE; } static BOOL IsWOWProcess( IN HINSTANCE hVdmDbg, IN DWORD dwProcessId ) { _ASSERTE(hVdmDbg != NULL); _ASSERTE(dwProcessId != 0); int (WINAPI * _VDMEnumProcessWOW)(PROCESSENUMPROC, LPARAM); *(FARPROC *)&_VDMEnumProcessWOW = GetProcAddress(hVdmDbg, "VDMEnumProcessWOW"); _ASSERTE(_VDMEnumProcessWOW != NULL); _VDMEnumProcessWOW(EnumProcessWOWProc, (LPARAM)&dwProcessId); return dwProcessId == 0; } |
IsWOWProcess перечисляет все виртуальные DOS-машины WOW с помощью функции VDMEnumProcessWOW из VDMDBG.DLL. Если окно, указанное в качестве параметра KillApplication, принадлежит одному из этих процессов, значит, мы имеем дело с 16-битной задачей.
Для 16-битных задач мы тоже рассылаем сообщение WM_CLOSE всем окнам верхнего уровня этой задачи, но теперь мы ориентируемся не по идентификатору процесса, а по идентификатору потока, поскольку процесс виртуальной DOS-машины может содержать несколько 16-битных задач, каждая из которых выполняется в своем потоке. Для рассылки WM_CLOSE мы используем EnumWindows с функцией KillAppEnumWindows16 в качестве функции перечисления:
static BOOL CALLBACK KillAppEnumWindows16( IN HWND hWnd, IN LPARAM lParam ) { _ASSERTE(lParam != 0); if (IsWindowVisible(hWnd) && GetWindowThreadProcessId(hWnd, NULL) == (DWORD)lParam) PostMessage(hWnd, WM_CLOSE, 0, 0); return TRUE; } |
В случае 16-битных задач мы не можем использовать описатель процесса для ожидания завершения задачи, поскольку процесс виртуальной DOS-машины может и не завершиться, если в нем остались другие задачи. Для определения факта завершения задачи мы используем функцию IsWOWTask, которая в качестве побочного эффекта возвращает идентификатор задачи.
typedef struct _WOWTASKENUM { DWORD dwThreadId; WORD wTaskId; } WOWTASKENUM, * PWOWTASKENUM; static BOOL CALLBACK EnumTaskWOWProc( IN DWORD dwThreadId, IN WORD hMod16, IN WORD hTask16, IN LPARAM lParam ) { PWOWTASKENUM pTaskEnum = (PWOWTASKENUM)lParam; _ASSERTE(pTaskEnum != NULL); if (dwThreadId == pTaskEnum->dwThreadId) { pTaskEnum->wTaskId = hTask16; return TRUE; } return FALSE; } static WORD IsWOWTask( IN HINSTANCE hVdmDbg, IN DWORD dwProcessId, IN DWORD dwThreadId ) { _ASSERTE(hVdmDbg != NULL); _ASSERTE(dwProcessId != 0); _ASSERTE(dwThreadId != 0); int (WINAPI * _VDMEnumTaskWOW)(DWORD, TASKENUMPROC, LPARAM); *(FARPROC *)&_VDMEnumTaskWOW = GetProcAddress(hVdmDbg, "VDMEnumTaskWOW"); WOWTASKENUM wte; wte.dwThreadId = dwThreadId; wte.wTaskId = 0; _VDMEnumTaskWOW(dwProcessId, EnumTaskWOWProc, (LPARAM)&wte); return wte.wTaskId; } |
IsWOWTask полагается на функцию VDMEnumTaskWOW, экспортируемую из VDMDBG.DLL, которая была рассмотрена в статье Как перечислить 16-битные задачи под Windows NT?.
Как и в случае 32-битных приложений, мы вызываем пользовательскую функцию с интервалом в 100 миллисекунд. Если пользовательская функция решает завершить задачу принудительно, мы вызываем функцию TerminateWOWTask, которая есть ни что иное как обертка вокруг VDMTerminateTaskWOW.
static BOOL TerminateWOWTask( IN HINSTANCE hVdmDbg, IN DWORD dwProcessId, IN WORD wTaskId ) { _ASSERTE(hVdmDbg != NULL); _ASSERTE(dwProcessId != 0); _ASSERTE(wTaskId != 0); BOOL (WINAPI * _VDMTerminateTaskWOW)(DWORD, WORD); *(FARPROC *)&_VDMTerminateTaskWOW = GetProcAddress(hVdmDbg, "VDMTerminateTaskWOW"); return _VDMTerminateTaskWOW(dwProcessId, wTaskId); } |
В заключение рассмотрим несколько вариантов реализации пользовательской функции для использования совместно с KillApplication. Первый вариант функции реализует ожидание с фиксированным таймаутом:
int CALLBACK WaitCallback1( IN LPARAM lParam ) { return (GetTickCount() < (DWORD)lParam) ? KILLAPP_WAIT : KILLAPP_TERMINATE; } |
Предполагается, что в качестве параметра lParam для KillApplication будет указано время окончания ожидания. Например, такой вызов приведет к 15-секундному ожиданию:
KillApplication(hWnd, WaitCallback1, GetTickCount() + 15000); |
Более сложный вариант пользовательской функции, приведенный ниже, обрабатывает сообщения в процессе ожидания, так что функцию можно вызывать непосредственно в потоке пользовательского интерфейса, не опасаясь того, что он будет заблокирован:
int CALLBACK WaitCallback2( IN LPARAM lParam ) { MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (GetTickCount() < (DWORD)lParam) ? KILLAPP_WAIT : KILLAPP_TERMINATE; } |
Еще более сложный вариант, который обрабатывает сообщения и отображает диалоговое окно по истечении 15 секунд ожидания, вы можете найти в исходном коде демонстрационного приложения Process Viewer, которое сопровождает эту статью.