Как программно завершить другое приложение?

Автор: Александр Федотов
Опубликовано: 02.11.2001
Версия текста: 1.0

Тестовое приложение 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, которое сопровождает эту статью.

Cсылки

  1. Q178893 HOWTO: Terminate an Application "Cleanly" in Win32, Microsoft Knowledge Base.

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