/*
 * Process Hacker -
 *   thread wait analysis
 *
 * Copyright (C) 2010-2011 wj32
 *
 * This file is part of Process Hacker.
 *
 * Process Hacker is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Process Hacker is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Process Hacker.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * There are two ways of seeing what a thread is waiting on. The first method
 * is to walk the stack of a thread and read the arguments to whatever system
 * call it is blocking on; this only works on x86 because on x64 the arguments
 * are passed in registers (at least the first four are). The second method
 * involves using the ThreadLastSystemCall info class for NtQueryInformationThread
 * to retrieve the first argument to the system call the thread is blocking on.
 * This is obviously only useful for NtWaitForSingleObject.
 *
 * There are other methods for specific scenarios, like USER messages and ALPC
 * calls.
 */

#include <phapp.h>
#include <procprv.h>
#include <symprv.h>
#include <hndlinfo.h>

typedef HWND (WINAPI *_GetSendMessageReceiver)(
    _In_ HANDLE ThreadId
    );

typedef NTSTATUS (NTAPI *_NtAlpcQueryInformation)(
    _In_ HANDLE PortHandle,
    _In_ ALPC_PORT_INFORMATION_CLASS PortInformationClass,
    _Out_writes_bytes_(Length) PVOID PortInformation,
    _In_ ULONG Length,
    _Out_opt_ PULONG ReturnLength
    );

typedef struct _ANALYZE_WAIT_CONTEXT
{
    BOOLEAN Found;
    BOOLEAN IsWow64;
    HANDLE ProcessId;
    HANDLE ThreadId;
    HANDLE ProcessHandle;

    PPH_SYMBOL_PROVIDER SymbolProvider;
    PH_STRING_BUILDER StringBuilder;

    PVOID PrevParams[4];
} ANALYZE_WAIT_CONTEXT, *PANALYZE_WAIT_CONTEXT;

VOID PhpAnalyzeWaitPassive(
    _In_ HWND hWnd,
    _In_ HANDLE ProcessId,
    _In_ HANDLE ThreadId
    );

BOOLEAN NTAPI PhpWalkThreadStackAnalyzeCallback(
    _In_ PPH_THREAD_STACK_FRAME StackFrame,
    _In_opt_ PVOID Context
    );

VOID PhpAnalyzeWaitFallbacks(
    _In_ PANALYZE_WAIT_CONTEXT Context
    );

VOID PhpInitializeServiceNumbers(
    VOID
    );

PPH_STRING PhpaGetHandleString(
    _In_ HANDLE ProcessHandle,
    _In_ HANDLE Handle
    );

VOID PhpGetWfmoInformation(
    _In_ HANDLE ProcessHandle,
    _In_ BOOLEAN IsWow64,
    _In_ ULONG NumberOfHandles,
    _In_ PHANDLE AddressOfHandles,
    _In_ WAIT_TYPE WaitType,
    _In_ BOOLEAN Alertable,
    _Inout_ PPH_STRING_BUILDER StringBuilder
    );

PPH_STRING PhpaGetSendMessageReceiver(
    _In_ HANDLE ThreadId
    );

PPH_STRING PhpaGetAlpcInformation(
    _In_ HANDLE ThreadId
    );

static PH_INITONCE ServiceNumbersInitOnce = PH_INITONCE_INIT;
static USHORT NumberForWfso = -1;
static USHORT NumberForWfmo = -1;
static USHORT NumberForRf = -1;

VOID PhUiAnalyzeWaitThread(
    _In_ HWND hWnd,
    _In_ HANDLE ProcessId,
    _In_ HANDLE ThreadId,
    _In_ PPH_SYMBOL_PROVIDER SymbolProvider
    )
{
    NTSTATUS status;
    HANDLE threadHandle;
#ifdef _WIN64
    HANDLE processHandle;
    BOOLEAN isWow64;
#endif
    CLIENT_ID clientId;
    ANALYZE_WAIT_CONTEXT context;

#ifdef _WIN64
    // Determine if the process is WOW64. If not, we use the passive method.

    if (!NT_SUCCESS(status = PhOpenProcess(&processHandle, ProcessQueryAccess, ProcessId)))
    {
        PhShowStatus(hWnd, L"Unable to open the process", status, 0);
        return;
    }

    if (!NT_SUCCESS(status = PhGetProcessIsWow64(processHandle, &isWow64)) || !isWow64)
    {
        PhpAnalyzeWaitPassive(hWnd, ProcessId, ThreadId);
        return;
    }

    NtClose(processHandle);
#endif

    if (!NT_SUCCESS(status = PhOpenThread(
        &threadHandle,
        ThreadQueryAccess | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME,
        ThreadId
        )))
    {
        PhShowStatus(hWnd, L"Unable to open the thread", status, 0);
        return;
    }

    context.ProcessId = ProcessId;
    context.ThreadId = ThreadId;

    context.ProcessHandle = SymbolProvider->ProcessHandle;
    context.SymbolProvider = SymbolProvider;
    PhInitializeStringBuilder(&context.StringBuilder, 100);

    clientId.UniqueProcess = ProcessId;
    clientId.UniqueThread = ThreadId;

    PhWalkThreadStack(
        threadHandle,
        SymbolProvider->ProcessHandle,
        &clientId,
        SymbolProvider,
        PH_WALK_I386_STACK,
        PhpWalkThreadStackAnalyzeCallback,
        &context
        );
    NtClose(threadHandle);

    PhpAnalyzeWaitFallbacks(&context);

    if (context.Found)
    {
        PhShowInformationDialog(hWnd, context.StringBuilder.String->Buffer, 0);
    }
    else
    {
        PhShowInformation(hWnd, L"The thread does not appear to be waiting.");
    }

    PhDeleteStringBuilder(&context.StringBuilder);
}

VOID PhpAnalyzeWaitPassive(
    _In_ HWND hWnd,
    _In_ HANDLE ProcessId,
    _In_ HANDLE ThreadId
    )
{
    NTSTATUS status;
    HANDLE processHandle;
    HANDLE threadHandle;
    THREAD_LAST_SYSCALL_INFORMATION lastSystemCall;
    PH_STRING_BUILDER stringBuilder;
    PPH_STRING string;

    PhpInitializeServiceNumbers();

    if (!NT_SUCCESS(status = PhOpenThread(&threadHandle, THREAD_GET_CONTEXT, ThreadId)))
    {
        PhShowStatus(hWnd, L"Unable to open the thread", status, 0);
        return;
    }

    if (!NT_SUCCESS(status = NtQueryInformationThread(
        threadHandle,
        ThreadLastSystemCall,
        &lastSystemCall,
        sizeof(THREAD_LAST_SYSCALL_INFORMATION),
        NULL
        )))
    {
        NtClose(threadHandle);
        PhShowInformation(hWnd, L"Unable to determine whether the thread is waiting.");
        return;
    }

    if (!NT_SUCCESS(status = PhOpenProcess(&processHandle, PROCESS_DUP_HANDLE, ProcessId)))
    {
        NtClose(threadHandle);
        PhShowStatus(hWnd, L"Unable to open the process", status, 0);
        return;
    }

    PhInitializeStringBuilder(&stringBuilder, 100);

    if (lastSystemCall.SystemCallNumber == NumberForWfso)
    {
        string = PhpaGetHandleString(processHandle, lastSystemCall.FirstArgument);

        PhAppendFormatStringBuilder(&stringBuilder, L"Thread is waiting for:\r\n");
        PhAppendStringBuilder(&stringBuilder, &string->sr);
    }
    else if (lastSystemCall.SystemCallNumber == NumberForWfmo)
    {
        PhAppendFormatStringBuilder(&stringBuilder, L"Thread is waiting for multiple (%u) objects.", PtrToUlong(lastSystemCall.FirstArgument));
    }
    else if (lastSystemCall.SystemCallNumber == NumberForRf)
    {
        string = PhpaGetHandleString(processHandle, lastSystemCall.FirstArgument);

        PhAppendFormatStringBuilder(&stringBuilder, L"Thread is waiting for file I/O:\r\n");
        PhAppendStringBuilder(&stringBuilder, &string->sr);
    }
    else
    {
        string = PhpaGetSendMessageReceiver(ThreadId);

        if (string)
        {
            PhAppendStringBuilder2(&stringBuilder, L"Thread is sending a USER message:\r\n");
            PhAppendStringBuilder(&stringBuilder, &string->sr);
        }
        else
        {
            string = PhpaGetAlpcInformation(ThreadId);

            if (string)
            {
                PhAppendStringBuilder2(&stringBuilder, L"Thread is waiting for an ALPC port:\r\n");
                PhAppendStringBuilder(&stringBuilder, &string->sr);
            }
        }
    }

    if (stringBuilder.String->Length == 0)
        PhAppendStringBuilder2(&stringBuilder, L"Unable to determine why the thread is waiting.");

    PhShowInformationDialog(hWnd, stringBuilder.String->Buffer, 0);

    PhDeleteStringBuilder(&stringBuilder);
    NtClose(processHandle);
    NtClose(threadHandle);
}

static BOOLEAN NTAPI PhpWalkThreadStackAnalyzeCallback(
    _In_ PPH_THREAD_STACK_FRAME StackFrame,
    _In_opt_ PVOID Context
    )
{
    PANALYZE_WAIT_CONTEXT context = (PANALYZE_WAIT_CONTEXT)Context;
    PPH_STRING name;

    name = PhGetSymbolFromAddress(
        context->SymbolProvider,
        (ULONG64)StackFrame->PcAddress,
        NULL,
        NULL,
        NULL,
        NULL
        );

    if (!name)
        return TRUE;

    context->Found = TRUE;

#define FUNC_MATCH(Name) PhStartsWithString2(name, L##Name, TRUE)
#define NT_FUNC_MATCH(Name) ( \
    PhStartsWithString2(name, L"ntdll.dll!Nt" L##Name, TRUE) || \
    PhStartsWithString2(name, L"ntdll.dll!Zw" L##Name, TRUE) \
    )

    if (!name)
    {
        // Dummy
    }
    else if (FUNC_MATCH("kernel32.dll!Sleep"))
    {
        PhAppendFormatStringBuilder(
            &context->StringBuilder,
            L"Thread is sleeping. Timeout: %u milliseconds.",
            PtrToUlong(StackFrame->Params[0])
            );
    }
    else if (NT_FUNC_MATCH("DelayExecution"))
    {
        BOOLEAN alertable = !!StackFrame->Params[0];
        PVOID timeoutAddress = StackFrame->Params[1];
        LARGE_INTEGER timeout;

        if (NT_SUCCESS(NtReadVirtualMemory(
            context->ProcessHandle,
            timeoutAddress,
            &timeout,
            sizeof(LARGE_INTEGER),
            NULL
            )))
        {
            if (timeout.QuadPart < 0)
            {
                PhAppendFormatStringBuilder(
                    &context->StringBuilder,
                    L"Thread is sleeping. Timeout: %I64u milliseconds.",
                    -timeout.QuadPart / PH_TIMEOUT_MS
                    );
            }
            else
            {
                // TODO
            }
        }
        else
        {
            PhAppendStringBuilder2(
                &context->StringBuilder,
                L"Thread is sleeping."
                );
        }
    }
    else if (NT_FUNC_MATCH("DeviceIoControlFile"))
    {
        HANDLE handle = (HANDLE)StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for an I/O control request:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (NT_FUNC_MATCH("FsControlFile"))
    {
        HANDLE handle = StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for a FS control request:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (NT_FUNC_MATCH("QueryObject"))
    {
        HANDLE handle = StackFrame->Params[0];

        // Use the KiFastSystemCall args if the handle we have is wrong.
        if ((ULONG_PTR)handle % 4 != 0 || !handle)
            handle = context->PrevParams[1];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is querying an object:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (NT_FUNC_MATCH("ReadFile") || NT_FUNC_MATCH("WriteFile"))
    {
        HANDLE handle = StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for file I/O:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (NT_FUNC_MATCH("RemoveIoCompletion"))
    {
        HANDLE handle = StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for an I/O completion port:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (
        NT_FUNC_MATCH("ReplyWaitReceivePort") ||
        NT_FUNC_MATCH("RequestWaitReplyPort") ||
        NT_FUNC_MATCH("AlpcSendWaitReceivePort")
        )
    {
        HANDLE handle = StackFrame->Params[0];
        PPH_STRING alpcInfo;

        PhAppendStringBuilder2(
            &context->StringBuilder,
            WindowsVersion >= WINDOWS_VISTA ? L"Thread is waiting for an ALPC port:\r\n" : L"Thread is waiting for a LPC port:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );

        if (alpcInfo = PhpaGetAlpcInformation(context->ThreadId))
        {
            PhAppendStringBuilder2(
                &context->StringBuilder,
                L"\r\n"
                );
            PhAppendStringBuilder(
                &context->StringBuilder,
                &alpcInfo->sr
                );
        }
    }
    else if (
        NT_FUNC_MATCH("SetHighWaitLowEventPair") ||
        NT_FUNC_MATCH("SetLowWaitHighEventPair") ||
        NT_FUNC_MATCH("WaitHighEventPair") ||
        NT_FUNC_MATCH("WaitLowEventPair")
        )
    {
        HANDLE handle = StackFrame->Params[0];

        if ((ULONG_PTR)handle % 4 != 0 || !handle)
            handle = context->PrevParams[1];

        PhAppendFormatStringBuilder(
            &context->StringBuilder,
            L"Thread is waiting (%s) for an event pair:\r\n",
            name->Buffer
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (
        FUNC_MATCH("user32.dll!NtUserGetMessage") ||
        FUNC_MATCH("user32.dll!NtUserWaitMessage")
        )
    {
        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for a USER message.\r\n"
            );
    }
    else if (FUNC_MATCH("user32.dll!NtUserMessageCall"))
    {
        PPH_STRING receiverString;

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is sending a USER message:\r\n"
            );

        receiverString = PhpaGetSendMessageReceiver(context->ThreadId);

        if (receiverString)
        {
            PhAppendStringBuilder(&context->StringBuilder, &receiverString->sr);
            PhAppendStringBuilder2(&context->StringBuilder, L"\r\n");
        }
        else
        {
            PhAppendStringBuilder2(&context->StringBuilder, L"Unknown.\r\n");
        }
    }
    else if (NT_FUNC_MATCH("WaitForDebugEvent"))
    {
        HANDLE handle = StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for a debug event:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (
        NT_FUNC_MATCH("WaitForKeyedEvent") ||
        NT_FUNC_MATCH("ReleaseKeyedEvent")
        )
    {
        HANDLE handle = StackFrame->Params[0];
        PVOID key = StackFrame->Params[1];

        PhAppendFormatStringBuilder(
            &context->StringBuilder,
            L"Thread is waiting (%s) for a keyed event (key 0x%Ix):\r\n",
            name->Buffer,
            key
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (
        NT_FUNC_MATCH("WaitForMultipleObjects") ||
        FUNC_MATCH("kernel32.dll!WaitForMultipleObjects")
        )
    {
        ULONG numberOfHandles = PtrToUlong(StackFrame->Params[0]);
        PVOID addressOfHandles = StackFrame->Params[1];
        WAIT_TYPE waitType = (WAIT_TYPE)StackFrame->Params[2];
        BOOLEAN alertable = !!StackFrame->Params[3];

        if (numberOfHandles > MAXIMUM_WAIT_OBJECTS)
        {
            numberOfHandles = PtrToUlong(context->PrevParams[1]);
            addressOfHandles = context->PrevParams[2];
            waitType = (WAIT_TYPE)context->PrevParams[3];
            alertable = FALSE;
        }

        PhpGetWfmoInformation(
            context->ProcessHandle,
            TRUE, // on x64 this function is only called for WOW64 processes
            numberOfHandles,
            addressOfHandles,
            waitType,
            alertable,
            &context->StringBuilder
            );
    }
    else if (
        NT_FUNC_MATCH("WaitForSingleObject") ||
        FUNC_MATCH("kernel32.dll!WaitForSingleObject")
        )
    {
        HANDLE handle = StackFrame->Params[0];
        BOOLEAN alertable = !!StackFrame->Params[1];

        if ((ULONG_PTR)handle % 4 != 0 || !handle)
        {
            handle = context->PrevParams[1];
            alertable = !!context->PrevParams[2];
        }

        PhAppendFormatStringBuilder(
            &context->StringBuilder,
            L"Thread is waiting (%s) for:\r\n",
            alertable ? L"alertable" : L"non-alertable"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else if (NT_FUNC_MATCH("WaitForWorkViaWorkerFactory"))
    {
        HANDLE handle = StackFrame->Params[0];

        PhAppendStringBuilder2(
            &context->StringBuilder,
            L"Thread is waiting for work from a worker factory:\r\n"
            );
        PhAppendStringBuilder(
            &context->StringBuilder,
            &PhpaGetHandleString(context->ProcessHandle, handle)->sr
            );
    }
    else
    {
        context->Found = FALSE;
    }

    PhDereferenceObject(name);
    memcpy(&context->PrevParams, StackFrame->Params, sizeof(StackFrame->Params));

    return !context->Found;
}

static VOID PhpAnalyzeWaitFallbacks(
    _In_ PANALYZE_WAIT_CONTEXT Context
    )
{
    PPH_STRING info;

    // We didn't detect NtUserMessageCall, but this may still apply due to another
    // win32k system call (e.g. from EnableWindow).
    if (!Context->Found && (info = PhpaGetSendMessageReceiver(Context->ThreadId)))
    {
        PhAppendStringBuilder2(
            &Context->StringBuilder,
            L"Thread is sending a USER message:\r\n"
            );
        PhAppendStringBuilder(&Context->StringBuilder, &info->sr);
        PhAppendStringBuilder2(&Context->StringBuilder, L"\r\n");

        Context->Found = TRUE;
    }

    // Nt(Alpc)ConnectPort doesn't get detected anywhere else.
    if (!Context->Found && (info = PhpaGetAlpcInformation(Context->ThreadId)))
    {
        PhAppendStringBuilder2(
            &Context->StringBuilder,
            L"Thread is waiting for an ALPC port:\r\n"
            );
        PhAppendStringBuilder(&Context->StringBuilder, &info->sr);
        PhAppendStringBuilder2(&Context->StringBuilder, L"\r\n");

        Context->Found = TRUE;
    }
}

static BOOLEAN PhpWaitUntilThreadIsWaiting(
    _In_ HANDLE ThreadHandle
    )
{
    ULONG attempts;
    BOOLEAN isWaiting = FALSE;
    THREAD_BASIC_INFORMATION basicInfo;

    if (!NT_SUCCESS(PhGetThreadBasicInformation(ThreadHandle, &basicInfo)))
        return FALSE;

    for (attempts = 0; attempts < 5; attempts++)
    {
        PVOID processes;
        PSYSTEM_PROCESS_INFORMATION processInfo;
        ULONG i;
        LARGE_INTEGER interval;

        interval.QuadPart = -100 * PH_TIMEOUT_MS;
        NtDelayExecution(FALSE, &interval);

        if (!NT_SUCCESS(PhEnumProcesses(&processes)))
            break;

        processInfo = PhFindProcessInformation(processes, basicInfo.ClientId.UniqueProcess);

        if (processInfo)
        {
            for (i = 0; i < processInfo->NumberOfThreads; i++)
            {
                if (
                    processInfo->Threads[i].ClientId.UniqueThread == basicInfo.ClientId.UniqueThread &&
                    processInfo->Threads[i].ThreadState == Waiting &&
                    (processInfo->Threads[i].WaitReason == UserRequest ||
                    processInfo->Threads[i].WaitReason == Executive)
                    )
                {
                    isWaiting = TRUE;
                    break;
                }
            }
        }

        PhFree(processes);

        if (isWaiting)
            break;

        interval.QuadPart = -500 * PH_TIMEOUT_MS;
        NtDelayExecution(FALSE, &interval);
    }

    return isWaiting;
}

static VOID PhpGetThreadLastSystemCallNumber(
    _In_ HANDLE ThreadHandle,
    _Out_ PUSHORT LastSystemCallNumber
    )
{
    THREAD_LAST_SYSCALL_INFORMATION lastSystemCall;

    if (NT_SUCCESS(NtQueryInformationThread(
        ThreadHandle,
        ThreadLastSystemCall,
        &lastSystemCall,
        sizeof(THREAD_LAST_SYSCALL_INFORMATION),
        NULL
        )))
    {
        *LastSystemCallNumber = lastSystemCall.SystemCallNumber;
    }
}

static NTSTATUS PhpWfsoThreadStart(
    _In_ PVOID Parameter
    )
{
    HANDLE eventHandle;
    LARGE_INTEGER timeout;

    eventHandle = Parameter;

    timeout.QuadPart = -5 * PH_TIMEOUT_SEC;
    NtWaitForSingleObject(eventHandle, FALSE, &timeout);

    return STATUS_SUCCESS;
}

static NTSTATUS PhpWfmoThreadStart(
    _In_ PVOID Parameter
    )
{
    HANDLE eventHandle;
    LARGE_INTEGER timeout;

    eventHandle = Parameter;

    timeout.QuadPart = -5 * PH_TIMEOUT_SEC;
    NtWaitForMultipleObjects(1, &eventHandle, WaitAll, FALSE, &timeout);

    return STATUS_SUCCESS;
}

static NTSTATUS PhpRfThreadStart(
    _In_ PVOID Parameter
    )
{
    HANDLE fileHandle;
    IO_STATUS_BLOCK isb;
    ULONG data;

    fileHandle = Parameter;

    NtReadFile(fileHandle, NULL, NULL, NULL, &isb, &data, sizeof(ULONG), NULL, NULL);

    return STATUS_SUCCESS;
}

static VOID PhpInitializeServiceNumbers(
    VOID
    )
{
    if (PhBeginInitOnce(&ServiceNumbersInitOnce))
    {
        NTSTATUS status;
        HANDLE eventHandle;
        HANDLE threadHandle;
        HANDLE pipeReadHandle;
        HANDLE pipeWriteHandle;

        // The ThreadLastSystemCall info class only works when the thread is in the Waiting
        // state. We'll create a thread which blocks on an event object we create, then wait
        // until it is in the Waiting state. Only then can we query the thread using
        // ThreadLastSystemCall.

        // NtWaitForSingleObject

        status = NtCreateEvent(&eventHandle, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE);

        if (NT_SUCCESS(status))
        {
            if (threadHandle = PhCreateThread(0, PhpWfsoThreadStart, eventHandle))
            {
                if (PhpWaitUntilThreadIsWaiting(threadHandle))
                {
                    PhpGetThreadLastSystemCallNumber(threadHandle, &NumberForWfso);
                }

                // Allow the thread to exit.
                NtSetEvent(eventHandle, NULL);
                NtClose(threadHandle);
            }

            NtClose(eventHandle);
        }

        // NtWaitForMultipleObjects

        status = NtCreateEvent(&eventHandle, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE);

        if (NT_SUCCESS(status))
        {
            if (threadHandle = PhCreateThread(0, PhpWfmoThreadStart, eventHandle))
            {
                if (PhpWaitUntilThreadIsWaiting(threadHandle))
                {
                    PhpGetThreadLastSystemCallNumber(threadHandle, &NumberForWfmo);
                }

                NtSetEvent(eventHandle, NULL);
                NtClose(threadHandle);
            }

            NtClose(eventHandle);
        }

        // NtReadFile

        if (CreatePipe(&pipeReadHandle, &pipeWriteHandle, NULL, 0))
        {
            if (threadHandle = PhCreateThread(0, PhpRfThreadStart, pipeReadHandle))
            {
                ULONG data = 0;
                IO_STATUS_BLOCK isb;

                if (PhpWaitUntilThreadIsWaiting(threadHandle))
                {
                    PhpGetThreadLastSystemCallNumber(threadHandle, &NumberForRf);
                }

                NtWriteFile(pipeWriteHandle, NULL, NULL, NULL, &isb, &data, sizeof(data), NULL, NULL);
                NtClose(threadHandle);
            }

            NtClose(pipeReadHandle);
            NtClose(pipeWriteHandle);
        }

        PhEndInitOnce(&ServiceNumbersInitOnce);
    }
}

static PPH_STRING PhpaGetHandleString(
    _In_ HANDLE ProcessHandle,
    _In_ HANDLE Handle
    )
{
    PPH_STRING typeName = NULL;
    PPH_STRING name = NULL;
    PPH_STRING result;

    PhGetHandleInformation(
        ProcessHandle,
        Handle,
        -1,
        NULL,
        &typeName,
        NULL,
        &name
        );
    PH_AUTO(typeName);
    PH_AUTO(name);

    if (typeName && name)
    {
        result = PhaFormatString(
            L"Handle 0x%Ix (%s): %s",
            Handle,
            typeName->Buffer,
            !PhIsNullOrEmptyString(name) ? name->Buffer : L"(unnamed object)"
            );
    }
    else
    {
        result = PhaFormatString(
            L"Handle 0x%Ix: (error querying handle)",
            Handle
            );
    }

    return result;
}

static VOID PhpGetWfmoInformation(
    _In_ HANDLE ProcessHandle,
    _In_ BOOLEAN IsWow64,
    _In_ ULONG NumberOfHandles,
    _In_ PHANDLE AddressOfHandles,
    _In_ WAIT_TYPE WaitType,
    _In_ BOOLEAN Alertable,
    _Inout_ PPH_STRING_BUILDER StringBuilder
    )
{
    NTSTATUS status;
    HANDLE handles[MAXIMUM_WAIT_OBJECTS];
    ULONG i;

    status = STATUS_SUCCESS;

    if (NumberOfHandles <= MAXIMUM_WAIT_OBJECTS)
    {
#ifdef _WIN64
        if (IsWow64)
        {
            ULONG handles32[MAXIMUM_WAIT_OBJECTS];

            if (NT_SUCCESS(status = NtReadVirtualMemory(
                ProcessHandle,
                AddressOfHandles,
                handles32,
                NumberOfHandles * sizeof(ULONG),
                NULL
                )))
            {
                for (i = 0; i < NumberOfHandles; i++)
                    handles[i] = UlongToHandle(handles32[i]);
            }
        }
        else
        {
#endif
            status = NtReadVirtualMemory(
                ProcessHandle,
                AddressOfHandles,
                handles,
                NumberOfHandles * sizeof(HANDLE),
                NULL
                );
#ifdef _WIN64
        }
#endif

        if (NT_SUCCESS(status))
        {
            PhAppendFormatStringBuilder(
                StringBuilder,
                L"Thread is waiting (%s, %s) for:\r\n",
                Alertable ? L"alertable" : L"non-alertable",
                WaitType == WaitAll ? L"wait all" : L"wait any"
                );

            for (i = 0; i < NumberOfHandles; i++)
            {
                PhAppendStringBuilder(
                    StringBuilder,
                    &PhpaGetHandleString(ProcessHandle, handles[i])->sr
                    );
                PhAppendStringBuilder2(
                    StringBuilder,
                    L"\r\n"
                    );
            }
        }
    }

    if (!NT_SUCCESS(status) || NumberOfHandles > MAXIMUM_WAIT_OBJECTS)
    {
        PhAppendStringBuilder2(
            StringBuilder,
            L"Thread is waiting for multiple objects."
            );
    }
}

static PPH_STRING PhpaGetSendMessageReceiver(
    _In_ HANDLE ThreadId
    )
{
    static _GetSendMessageReceiver GetSendMessageReceiver_I;

    HWND windowHandle;
    ULONG threadId;
    ULONG processId;
    CLIENT_ID clientId;
    PPH_STRING clientIdName;
    WCHAR windowClass[64];
    PPH_STRING windowText;

    // GetSendMessageReceiver is an undocumented function exported by
    // user32.dll. It retrieves the handle of the window which a thread
    // is sending a message to.

    if (!GetSendMessageReceiver_I)
        GetSendMessageReceiver_I = PhGetModuleProcAddress(L"user32.dll", "GetSendMessageReceiver");

    if (!GetSendMessageReceiver_I)
        return NULL;

    windowHandle = GetSendMessageReceiver_I(ThreadId);

    if (!windowHandle)
        return NULL;

    threadId = GetWindowThreadProcessId(windowHandle, &processId);

    clientId.UniqueProcess = UlongToHandle(processId);
    clientId.UniqueThread = UlongToHandle(threadId);
    clientIdName = PH_AUTO(PhGetClientIdName(&clientId));

    if (!GetClassName(windowHandle, windowClass, sizeof(windowClass) / sizeof(WCHAR)))
        windowClass[0] = 0;

    windowText = PH_AUTO(PhGetWindowText(windowHandle));

    return PhaFormatString(L"Window 0x%Ix (%s): %s \"%s\"", windowHandle, clientIdName->Buffer, windowClass, PhGetStringOrEmpty(windowText));
}

static PPH_STRING PhpaGetAlpcInformation(
    _In_ HANDLE ThreadId
    )
{
    static _NtAlpcQueryInformation NtAlpcQueryInformation_I;

    NTSTATUS status;
    PPH_STRING string = NULL;
    HANDLE threadHandle;
    PALPC_SERVER_INFORMATION serverInfo;
    ULONG bufferLength;

    if (!NtAlpcQueryInformation_I)
        NtAlpcQueryInformation_I = PhGetModuleProcAddress(L"ntdll.dll", "NtAlpcQueryInformation");

    if (!NtAlpcQueryInformation_I)
        return NULL;

    if (!NT_SUCCESS(PhOpenThread(&threadHandle, THREAD_QUERY_INFORMATION, ThreadId)))
        return NULL;

    bufferLength = 0x110;
    serverInfo = PhAllocate(bufferLength);
    serverInfo->In.ThreadHandle = threadHandle;

    status = NtAlpcQueryInformation_I(NULL, AlpcServerInformation, serverInfo, bufferLength, &bufferLength);

    if (status == STATUS_INFO_LENGTH_MISMATCH)
    {
        PhFree(serverInfo);
        serverInfo = PhAllocate(bufferLength);
        serverInfo->In.ThreadHandle = threadHandle;

        status = NtAlpcQueryInformation_I(NULL, AlpcServerInformation, serverInfo, bufferLength, &bufferLength);
    }

    if (NT_SUCCESS(status) && serverInfo->Out.ThreadBlocked)
    {
        CLIENT_ID clientId;
        PPH_STRING clientIdName;

        clientId.UniqueProcess = serverInfo->Out.ConnectedProcessId;
        clientId.UniqueThread = NULL;
        clientIdName = PH_AUTO(PhGetClientIdName(&clientId));

        string = PhaFormatString(L"ALPC Port: %.*s (%s)", serverInfo->Out.ConnectionPortName.Length / 2, serverInfo->Out.ConnectionPortName.Buffer, clientIdName->Buffer);
    }

    PhFree(serverInfo);
    NtClose(threadHandle);

    return string;
}
