/*
 * Process Hacker Extra Plugins -
 *   Debug View Plugin
 *
 * Copyright (C) 2015 dmex
 *
 * 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/>.
 */

#include "main.h"

PH_CALLBACK_DECLARE(DbgLoggedCallback);

VOID DbgFreeLogEntry(
    _Inout_ PDEBUG_LOG_ENTRY Entry
    )
{
    PhClearReference(&Entry->FilePath);
    PhClearReference(&Entry->ProcessName);
    PhClearReference(&Entry->Message);

    PhFree(Entry);
}

VOID DbgAddLogEntry(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context,
    _In_ PDEBUG_LOG_ENTRY Entry
    )
{
    if (Context->LogMessageList->Count > PhGetIntegerSetting(SETTING_NAME_MAX_ENTRIES))
    {
        DbgFreeLogEntry(Context->LogMessageList->Items[0]);
        PhRemoveItemList(Context->LogMessageList, 0);
    }

    PhAddItemList(Context->LogMessageList, Entry);
    PhInvokeCallback(&DbgLoggedCallback, Entry);
}

VOID DbgClearLogEntries(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context
    )
{
    for (ULONG i = 0; i < Context->LogMessageList->Count; i++)
    {
        if (Context->LogMessageList->Items[i])
        {
            DbgFreeLogEntry(Context->LogMessageList->Items[i]);
        }
    }

    PhClearList(Context->LogMessageList);
}

VOID DbgShowErrorMessage(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context,
    _In_ PWSTR Type
    )
{
    ULONG errorCode = GetLastError();
    PPH_STRING errorMessage = PhGetWin32Message(errorCode);

    if (errorMessage)
    {
        PhShowError(Context->DialogHandle, PhaFormatString(L"%s: [%u] %s", Type, errorCode, errorMessage->Buffer)->Buffer);
        PhDereferenceObject(errorMessage);
    }
}

VOID DbgFormatObjectName(
    _In_ BOOLEAN LocalName,
    _In_ PWSTR OriginalName,
    _Out_ PUNICODE_STRING ObjectName
    )
{
    SIZE_T length;
    SIZE_T originalNameLength;

    // Sessions other than session 0 require SeCreateGlobalPrivilege.
    if (LocalName && NtCurrentPeb()->SessionId != 0)
    {
        WCHAR buffer[256] = L"";

        memcpy(buffer, L"\\Sessions\\", 10 * sizeof(WCHAR));
        _ultow(NtCurrentPeb()->SessionId, buffer + 10, 10);
        length = PhCountStringZ(buffer);
        originalNameLength = PhCountStringZ(OriginalName);
        memcpy(buffer + length, OriginalName, (originalNameLength + 1) * sizeof(WCHAR));
        length += originalNameLength;

        ObjectName->Buffer = buffer;
        ObjectName->MaximumLength = (ObjectName->Length = (USHORT)(length * sizeof(WCHAR))) + sizeof(WCHAR);
    }
    else
    {
        RtlInitUnicodeString(ObjectName, OriginalName);
    }
}

VOID DbgProcessLogMessageEntry(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context,
    _In_ BOOLEAN GlobalEvents
    )
{
    NTSTATUS status;
    PDBWIN_PAGE_BUFFER debugMessageBuffer;
    PDEBUG_LOG_ENTRY entry = NULL;
    HANDLE processHandle = NULL;
    PPH_STRING fileName = NULL;
    HICON icon = NULL;

    debugMessageBuffer = GlobalEvents ? Context->GlobalDebugBuffer : Context->LocalDebugBuffer;

    entry = PhAllocate(sizeof(DEBUG_LOG_ENTRY));
    memset(entry, 0, sizeof(DEBUG_LOG_ENTRY));

    PhQuerySystemTime(&entry->Time);
    entry->ProcessId = UlongToHandle(debugMessageBuffer->ProcessId);
    entry->Message = PhConvertMultiByteToUtf16(debugMessageBuffer->Buffer);

    if (WINDOWS_HAS_IMAGE_FILE_NAME_BY_PROCESS_ID)
    {
        status = PhGetProcessImageFileNameByProcessId(entry->ProcessId, &fileName);
    }
    else
    {
        if (NT_SUCCESS(status = PhOpenProcess(&processHandle, ProcessQueryAccess, entry->ProcessId)))
        {
            status = PhGetProcessImageFileName(processHandle, &fileName);
            NtClose(processHandle);
        }
    }

    if (!NT_SUCCESS(status))
        fileName = PhGetKernelFileName();

    PhMoveReference(&fileName, PhGetFileName(fileName));

    icon = PhGetFileShellIcon(PhGetString(fileName), L".exe", TRUE);

    if (icon)
    {
        entry->ImageIndex = ImageList_AddIcon(Context->ListViewImageList, icon);
        DestroyIcon(icon);
    }

    entry->FilePath = fileName;
    entry->ProcessName = PhGetBaseName(fileName);

    // Drop event if it matches a filter
    for (ULONG i = 0; i < Context->ExcludeList->Count; i++)
    {
        PDBG_FILTER_TYPE filterEntry = Context->ExcludeList->Items[i];

        if (filterEntry->Type == FilterByName)
        {
            if (PhEqualString(filterEntry->ProcessName, entry->ProcessName, TRUE))
            {
                DbgFreeLogEntry(entry);
                return;
            }
        }
        else if (filterEntry->Type == FilterByPid)
        {
            if (filterEntry->ProcessId == entry->ProcessId)
            {
                DbgFreeLogEntry(entry);
                return;
            }
        }
    }

    DbgAddLogEntry(Context, entry);
}

NTSTATUS DbgEventsLocalThread(
    _In_ PVOID Parameter
    )
{
    LARGE_INTEGER timeout;
    NTSTATUS status;
    PPH_DBGEVENTS_CONTEXT context = (PPH_DBGEVENTS_CONTEXT)Parameter;

    while (TRUE)
    {
        NtSetEvent(context->LocalBufferReadyEvent, NULL);

        status = NtWaitForSingleObject(
            context->LocalDataReadyEvent,
            FALSE,
            PhTimeoutFromMilliseconds(&timeout, 100)
            );

        if (status == STATUS_TIMEOUT)
            continue;
        if (status != STATUS_SUCCESS)
            break;

        // The process calling OutputDebugString is blocked here...
        // This gives us some time to extract information without the process exiting.
        DbgProcessLogMessageEntry(context, FALSE);
    }

    return STATUS_SUCCESS;
}

NTSTATUS DbgEventsGlobalThread(
    _In_ PVOID Parameter
    )
{
    LARGE_INTEGER timeout;
    NTSTATUS status;
    PPH_DBGEVENTS_CONTEXT context = (PPH_DBGEVENTS_CONTEXT)Parameter;

    while (TRUE)
    {
        NtSetEvent(context->GlobalBufferReadyEvent, NULL);

        status = NtWaitForSingleObject(
            context->GlobalDataReadyEvent,
            FALSE,
            PhTimeoutFromMilliseconds(&timeout, 100)
            );

        if (status == STATUS_TIMEOUT)
            continue;
        if (status != STATUS_SUCCESS)
            break;

        // The process calling OutputDebugString is blocked here...
        // This gives us some time to extract information without the process exiting.
        DbgProcessLogMessageEntry(context, TRUE);
    }

    return STATUS_SUCCESS;
}

BOOLEAN DbgCreateSecurityAttributes(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context
    )
{
    Context->SecurityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
    Context->SecurityAttributes.bInheritHandle = TRUE;

    if (ConvertStringSecurityDescriptorToSecurityDescriptor(
        L"D:(A;;GRGWGX;;;WD)(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGWGX;;;AN)(A;;GRGWGX;;;RC)(A;;GRGWGX;;;S-1-15-2-1)S:(ML;;NW;;;LW)",
        SDDL_REVISION,
        &Context->SecurityAttributes.lpSecurityDescriptor,
        NULL
        ))
    {
        return TRUE;
    }

    return FALSE;
}

BOOLEAN DbgCleanupSecurityAttributes(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context
    )
{
    if (Context->SecurityAttributes.lpSecurityDescriptor)
    {
        LocalFree(Context->SecurityAttributes.lpSecurityDescriptor);
        Context->SecurityAttributes.lpSecurityDescriptor = NULL;
        return TRUE;
    }

    return FALSE;
}

BOOLEAN DbgEventsCreate(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context,
    _In_ BOOLEAN GlobalEvents
    )
{
    if (GlobalEvents)
    {
        SIZE_T viewSize;
        LARGE_INTEGER maximumSize;
        OBJECT_ATTRIBUTES objectAttributes;
        UNICODE_STRING objectName;
        HANDLE threadHandle = NULL;

        maximumSize.QuadPart = PAGE_SIZE;
        viewSize = sizeof(DBWIN_PAGE_BUFFER);

        if (!(Context->GlobalBufferReadyEvent = CreateEvent(&Context->SecurityAttributes, FALSE, FALSE, L"Global\\" DBWIN_BUFFER_READY)))
        {
            DbgShowErrorMessage(Context, L"DBWIN_BUFFER_READY");
            return FALSE;
        }

        if (!(Context->GlobalDataReadyEvent = CreateEvent(&Context->SecurityAttributes, FALSE, FALSE, L"Global\\" DBWIN_DATA_READY)))
        {
            DbgShowErrorMessage(Context, L"DBWIN_DATA_READY");
            return FALSE;
        }

        DbgFormatObjectName(FALSE, DBWIN_BUFFER_SECTION_NAME, &objectName);
        InitializeObjectAttributes(
            &objectAttributes,
            &objectName,
            OBJ_CASE_INSENSITIVE,
            NULL,
            Context->SecurityAttributes.lpSecurityDescriptor
            );

        if (!NT_SUCCESS(NtCreateSection(
            &Context->GlobalDataBufferHandle,
            STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_WRITE,
            &objectAttributes,
            &maximumSize,
            PAGE_READWRITE,
            SEC_COMMIT,
            NULL
            )))
        {
            DbgShowErrorMessage(Context, L"NtCreateSection");
            return FALSE;
        }

        if (!NT_SUCCESS(NtMapViewOfSection(
            Context->GlobalDataBufferHandle,
            NtCurrentProcess(),
            &Context->GlobalDebugBuffer,
            0,
            0,
            NULL,
            &viewSize,
            ViewShare,
            0,
            PAGE_READONLY
            )))
        {
            DbgShowErrorMessage(Context, L"NtMapViewOfSection");
            return FALSE;
        }

        Context->CaptureGlobalEnabled = TRUE;

        if (threadHandle = PhCreateThread(0, DbgEventsGlobalThread, Context))
        {
            NtClose(threadHandle);
        }
    }
    else
    {
        SIZE_T viewSize;
        LARGE_INTEGER maximumSize;
        OBJECT_ATTRIBUTES objectAttributes;
        UNICODE_STRING objectName;
        HANDLE threadHandle = NULL;

        maximumSize.QuadPart = PAGE_SIZE;
        viewSize = sizeof(DBWIN_PAGE_BUFFER);

        if (!(Context->LocalBufferReadyEvent = CreateEvent(&Context->SecurityAttributes, FALSE, FALSE, L"Local\\" DBWIN_BUFFER_READY)))
        {
            DbgShowErrorMessage(Context, L"DBWIN_BUFFER_READY");
            return FALSE;
        }

        if (!(Context->LocalDataReadyEvent = CreateEvent(&Context->SecurityAttributes, FALSE, FALSE, L"Local\\" DBWIN_DATA_READY)))
        {
            DbgShowErrorMessage(Context, L"DBWIN_DATA_READY");
            return FALSE;
        }

        DbgFormatObjectName(TRUE, DBWIN_BUFFER_SECTION_NAME, &objectName);
        InitializeObjectAttributes(
            &objectAttributes,
            &objectName,
            OBJ_CASE_INSENSITIVE,
            NULL,
            Context->SecurityAttributes.lpSecurityDescriptor
            );

        if (!NT_SUCCESS(NtCreateSection(
            &Context->LocalDataBufferHandle,
            STANDARD_RIGHTS_REQUIRED | SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_WRITE,
            &objectAttributes,
            &maximumSize,
            PAGE_READWRITE,
            SEC_COMMIT,
            NULL
            )))
        {
            DbgShowErrorMessage(Context, L"NtCreateSection");
            return FALSE;
        }

        if (!NT_SUCCESS(NtMapViewOfSection(
            Context->LocalDataBufferHandle,
            NtCurrentProcess(),
            &Context->LocalDebugBuffer,
            0,
            0,
            NULL,
            &viewSize,
            ViewShare,
            0,
            PAGE_READONLY
            )))
        {
            DbgShowErrorMessage(Context, L"NtMapViewOfSection");
            return FALSE;
        }

        Context->CaptureLocalEnabled = TRUE;

        if (threadHandle = PhCreateThread(0, DbgEventsLocalThread, Context))
        {
            NtClose(threadHandle);
        }
    }

    return TRUE;
}

VOID DbgEventsCleanup(
    _Inout_ PPH_DBGEVENTS_CONTEXT Context,
    _In_ BOOLEAN CleanupGlobal
    )
{
    if (CleanupGlobal)
    {
        Context->CaptureGlobalEnabled = FALSE;

        if (Context->GlobalDebugBuffer)
        {
            NtUnmapViewOfSection(NtCurrentProcess(), Context->GlobalDebugBuffer);
            Context->GlobalDebugBuffer = NULL;
        }

        if (Context->GlobalDataBufferHandle)
        {
            NtClose(Context->GlobalDataBufferHandle);
            Context->GlobalDataBufferHandle = NULL;
        }

        if (Context->GlobalBufferReadyEvent)
        {
            NtClose(Context->GlobalBufferReadyEvent);
            Context->GlobalBufferReadyEvent = NULL;
        }

        if (Context->GlobalDataReadyEvent)
        {
            NtClose(Context->GlobalDataReadyEvent);
            Context->GlobalDataReadyEvent = NULL;
        }
    }
    else
    {
        Context->CaptureLocalEnabled = FALSE;

        if (Context->LocalDebugBuffer)
        {
            NtUnmapViewOfSection(NtCurrentProcess(), Context->LocalDebugBuffer);
            Context->LocalDebugBuffer = NULL;
        }

        if (Context->LocalDataBufferHandle)
        {
            NtClose(Context->LocalDataBufferHandle);
            Context->LocalDataBufferHandle = NULL;
        }

        if (Context->LocalBufferReadyEvent)
        {
            NtClose(Context->LocalBufferReadyEvent);
            Context->LocalBufferReadyEvent = NULL;
        }

        if (Context->LocalDataReadyEvent)
        {
            NtClose(Context->LocalDataReadyEvent);
            Context->LocalDataReadyEvent = NULL;
        }
    }
}