AuroraRuntime/Source/Debug/ExceptionWatcher.Win32.cpp

583 lines
17 KiB
C++

/***
Copyright (C) 2021 J Reece Wilson (a/k/a "Reece"). All rights reserved.
File: ExceptionWatcher.Win32.cpp
Date: 2021-6-12
Author: Reece
***/
#include <Source/RuntimeInternal.hpp>
#include "Debug.hpp"
#include "ExceptionWatcher.NT.hpp"
#include "ExceptionWatcher.Win32.hpp"
#include "Stack.Win32.hpp"
#include <Source/Process/AuProcessMap.Win32.hpp>
#include <Source/Telemetry/Telemetry.hpp>
#include <Windows.h>
#include <Dbghelp.h>
#include <codecvt>
#include <vcruntime_exception.h>
#include <ehdata.h>
#include <Source/Process/AuProcessMap.hpp>
#include <Source/IO/FS/FS.hpp>
#include <Source/Grug/AuGrug.hpp>
#include <Source/Exit/AuExit.hpp>
#if 0
#include <WerApi.h>
#endif
static thread_local int gDebugLocked = 0;
namespace Aurora::Debug
{
#define EXCEPTION_ENTRY(n) {n, #n}
static const AuHashMap<DWORD, AuString> kExceptionTable
{
EXCEPTION_ENTRY(STILL_ACTIVE),
EXCEPTION_ENTRY(EXCEPTION_ACCESS_VIOLATION),
EXCEPTION_ENTRY(EXCEPTION_DATATYPE_MISALIGNMENT),
EXCEPTION_ENTRY(EXCEPTION_BREAKPOINT),
EXCEPTION_ENTRY(EXCEPTION_SINGLE_STEP),
EXCEPTION_ENTRY(EXCEPTION_ARRAY_BOUNDS_EXCEEDED),
EXCEPTION_ENTRY(EXCEPTION_FLT_DENORMAL_OPERAND),
EXCEPTION_ENTRY(EXCEPTION_FLT_DIVIDE_BY_ZERO),
EXCEPTION_ENTRY(EXCEPTION_FLT_INEXACT_RESULT),
EXCEPTION_ENTRY(EXCEPTION_FLT_INVALID_OPERATION),
EXCEPTION_ENTRY(EXCEPTION_FLT_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_FLT_STACK_CHECK),
EXCEPTION_ENTRY(EXCEPTION_FLT_UNDERFLOW),
EXCEPTION_ENTRY(EXCEPTION_INT_DIVIDE_BY_ZERO),
EXCEPTION_ENTRY(EXCEPTION_INT_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_PRIV_INSTRUCTION),
EXCEPTION_ENTRY(EXCEPTION_IN_PAGE_ERROR),
EXCEPTION_ENTRY(EXCEPTION_ILLEGAL_INSTRUCTION),
EXCEPTION_ENTRY(EXCEPTION_NONCONTINUABLE_EXCEPTION),
EXCEPTION_ENTRY(EXCEPTION_STACK_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_INVALID_DISPOSITION),
EXCEPTION_ENTRY(EXCEPTION_GUARD_PAGE),
EXCEPTION_ENTRY(EXCEPTION_INVALID_HANDLE),
//EXCEPTION_ENTRY(EXCEPTION_POSSIBLE_DEADLOCK),
EXCEPTION_ENTRY(CONTROL_C_EXIT),
EXCEPTION_ENTRY(DBG_CONTROL_C)
};
#undef EXCEPTION_ENTRY
#define EXCEPTION_ENTRY(n) {n, true}
static const AuHashMap<DWORD, bool> kExceptionFatalTable
{
EXCEPTION_ENTRY(EXCEPTION_ACCESS_VIOLATION),
EXCEPTION_ENTRY(EXCEPTION_DATATYPE_MISALIGNMENT),
EXCEPTION_ENTRY(EXCEPTION_ARRAY_BOUNDS_EXCEEDED),
EXCEPTION_ENTRY(EXCEPTION_FLT_DENORMAL_OPERAND),
EXCEPTION_ENTRY(EXCEPTION_FLT_DIVIDE_BY_ZERO),
EXCEPTION_ENTRY(EXCEPTION_FLT_INEXACT_RESULT),
EXCEPTION_ENTRY(EXCEPTION_FLT_INVALID_OPERATION),
EXCEPTION_ENTRY(EXCEPTION_FLT_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_FLT_STACK_CHECK),
EXCEPTION_ENTRY(EXCEPTION_FLT_UNDERFLOW),
EXCEPTION_ENTRY(EXCEPTION_INT_DIVIDE_BY_ZERO),
EXCEPTION_ENTRY(EXCEPTION_INT_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_PRIV_INSTRUCTION),
EXCEPTION_ENTRY(EXCEPTION_IN_PAGE_ERROR),
EXCEPTION_ENTRY(EXCEPTION_ILLEGAL_INSTRUCTION),
EXCEPTION_ENTRY(EXCEPTION_NONCONTINUABLE_EXCEPTION),
EXCEPTION_ENTRY(EXCEPTION_STACK_OVERFLOW),
EXCEPTION_ENTRY(EXCEPTION_INVALID_DISPOSITION),
EXCEPTION_ENTRY(EXCEPTION_GUARD_PAGE)
};
#undef EXCEPTION_ENTRY
void PlatformHandleFatalEx2(bool fatal, CONTEXT &ctx, bool bNoExit);
static bool IsReadable(const void *address)
{
MEMORY_BASIC_INFORMATION info;
if (!VirtualQuery(address, &info, sizeof(info)))
{
return false;
}
if (!info.BaseAddress)
{
return false;
}
return (info.Protect & (PAGE_READONLY | PAGE_READWRITE)) != 0;
}
bool InPanic();
static LONG CALLBACK HandleVectorException(_EXCEPTION_POINTERS *ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == DBG_CONTROL_C)
{
Exit::PostLevel(AuThreads::GetThread(), Exit::ETriggerLevel::eSigTerminate);
return AuExchange(Exit::gHasCanceled, false) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
// https://www.youtube.com/embed/w6P03sTzSqM?start=2&autoplay=1
if (ExceptionInfo->ExceptionRecord->ExceptionCode < STATUS_GUARD_PAGE_VIOLATION)
{
return EXCEPTION_CONTINUE_SEARCH;
}
#if defined(AU_CFG_ID_SHIP)
// you what?
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT)
{
SysPanic("");
return EXCEPTION_CONTINUE_EXECUTION;
}
#else
// debugger go brrr
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT)
{
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
// debug builds can go do something stupid
// QA builds, staging, rel in any form should just give up trying if we're under a panic
#if !defined(AU_CFG_ID_DEBUG)
if (InPanic())
{
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
// something dumb like this mess w hat i had before.
// gave up trying
auto minimal = gDebugLocked++;
if (minimal >= 5)
{
SysPanic("Nested Exception");
}
bool cxxThrow = ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == EH_MAGIC_NUMBER1;
bool cxxThrowPure = ExceptionInfo->ExceptionRecord->ExceptionInformation[0] == EH_PURE_MAGIC_NUMBER1;
void *exception {};
HMODULE handle {};
ThrowInfo *pThrowInfo {};
if ((ExceptionInfo->ExceptionRecord->ExceptionCode == EH_EXCEPTION_NUMBER) &&
(ExceptionInfo->ExceptionRecord->NumberParameters >= 3) &&
((cxxThrow) ||
(cxxThrowPure))
)
{
pThrowInfo = reinterpret_cast<ThrowInfo *>(ExceptionInfo->ExceptionRecord->ExceptionInformation[2]);
if (pThrowInfo)
{
auto attribs = pThrowInfo->attributes;
if (_EH_RELATIVE_TYPEINFO)
{
handle = reinterpret_cast<HMODULE>(ExceptionInfo->ExceptionRecord->ExceptionInformation[3]);
}
exception = reinterpret_cast<void *>(ExceptionInfo->ExceptionRecord->ExceptionInformation[1]);
}
}
bool isCritical = AuExists(kExceptionFatalTable, ExceptionInfo->ExceptionRecord->ExceptionCode);
auto handleNoCppObject = [&]() -> AuString
{
const AuString *msg;
if (AuTryFind(kExceptionTable, ExceptionInfo->ExceptionRecord->ExceptionCode, msg))
{
return *msg;
}
else
{
return AuToString(ExceptionInfo->ExceptionRecord->ExceptionCode);
}
};
StackTrace backtrace;
{
static bool bRunOnce {};
struct ArrowEx : Grug::Arrow
{
AuFunction<void()> callback;
};
ArrowEx empty;
empty.callback = [&]()
{
try
{
if (AuExchange(bRunOnce, true))
{
Telemetry::Mayday();
}
ParseStack(ExceptionInfo->ContextRecord, backtrace);
}
catch (...)
{
}
};
empty.pCallback =[](Grug::Arrow *pBase) -> void
{
auto pEx = (ArrowEx *)pBase;
pEx->callback();
};
Grug::HurlArrow(&empty, empty.pCallback, {});
Grug::ArrowWait(&empty);
}
CONTEXT ctx {};
ctx.ContextFlags = CONTEXT_ALL;
if (!GetThreadContext(GetCurrentThread(), &ctx))
{
Debug::Panic();
}
AuVoidFunc doReportLocal;
{
#if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG)
const bool kShouldPrintErrors = true;
#else
const bool kShouldPrintErrors = false;
#endif
doReportLocal = [&]()
{
ReportSEH(handle, exception, pThrowInfo, handleNoCppObject, backtrace, [&](const AuString &str)
{
// Pre-submit callback -> its showtime
if ((isCritical || kShouldPrintErrors) && (minimal == 0))
{
if (gRuntimeConfig.debug.bPrintExceptionStackTracesOut)
{
AuLogWarn("NT Exception: 0x{:x}, {}", ExceptionInfo->ExceptionRecord->ExceptionCode, str);
AuLogWarn("{}", StringifyStackTrace(backtrace));
}
}
});
};
}
auto pThread = AuThreads::GetThread();
{
static bool bRunOnce {};
struct ArrowEx : Grug::Arrow
{
AuFunction<void()> callback;
};
ArrowEx empty;
empty.callback = [&]()
{
doReportLocal();
ReportStackTrace(backtrace, "");
Exit::PostLevel(pThread, Aurora::Exit::ETriggerLevel::eFatalException);
try
{
if (isCritical || gRuntimeConfig.debug.bIsExceptionThrowFatal) // exception = literally anything
{
PlatformHandleFatalEx2(true, ctx, true);
}
else
{
PlatformHandleFatalEx2(false, ctx, true);
}
}
catch (...)
{
}
};
empty.pCallback =[](Grug::Arrow *pBase) -> void
{
auto pEx = (ArrowEx *)pBase;
pEx->callback();
};
Grug::HurlArrow(&empty, empty.pCallback, {});
Grug::ArrowWait(&empty);
ReportStackTrace(backtrace, "");
}
gDebugLocked = 0;
return EXCEPTION_CONTINUE_SEARCH;
}
static AuString GetDumpName()
{
AuString exeName;
try
{
Process::GetProcName(exeName);
}
catch (...)
{
}
try
{
auto tm = Time::ToCivilTime(Time::CurrentClockMS());
return AuLocale::TimeDateToFileNameISO8601(tm) + ".dmp";
}
catch (...)
{
return "errordate.dmp";
}
}
#if defined(AU_ENABLE_NATIVE_MINIDUMP)
void SaveMinidump(_EXCEPTION_POINTERS *ExceptionInfo, bool isFatal)
{
bool ok {};
MINIDUMP_EXCEPTION_INFORMATION info {};
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = false;
info.ExceptionPointers = ExceptionInfo;
static const DWORD flags = MiniDumpWithFullMemory |
MiniDumpWithFullMemoryInfo |
MiniDumpWithHandleData |
MiniDumpWithUnloadedModules |
MiniDumpWithThreadInfo;
std::wstring path;
AuString utf8Path;
while (path.empty())
{
try
{
utf8Path = AuIOFS::NormalizePathRet("./Logs/Crashes/" + GetDumpName());
path = Locale::ConvertFromUTF8(utf8Path);
}
catch (...)
{
}
}
AuIOFS::CreateDirectories(utf8Path, true);
auto hFile = Win32Open(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, false, CREATE_ALWAYS, 0, FILE_ATTRIBUTE_NORMAL);
if (hFile == INVALID_HANDLE_VALUE)
{
AuLogWarn("Couldn't open minidump file. Has a debugger locked the .dmp file?");
goto miniDumpOut;
}
if (!pMiniDumpWriteDump)
{
goto miniDumpOut;
}
ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr);
if (!ok)
{
AuLogWarn("Couldn't write minidump: {:x}", GetLastError());
goto miniDumpOut;
}
CloseHandle(hFile);
miniDumpOut:
try
{
AuDebug::PrintError();
}
catch (...)
{
}
if (isFatal)
{
try
{
Grug::GrugFlushFlushs();
Grug::GrugFlushWrites();
}
catch (...)
{
}
Win32Terminate();
}
}
#endif
void BlackboxReport(_EXCEPTION_POINTERS *ExceptionInfo, bool fatal)
{
HANDLE hFile;
std::wstring path;
AuString utf8Path;
bool ok { true };
MINIDUMP_EXCEPTION_INFORMATION info;
if (fatal)
{
AuDebug::AddMemoryCrunch();
}
auto dumpName = GetDumpName();
info.ClientPointers = false;
info.ThreadId = GetCurrentThreadId();
info.ExceptionPointers = ExceptionInfo;
static const DWORD flags = MiniDumpWithDataSegs |
MiniDumpWithFullMemoryInfo |
MiniDumpWithHandleData |
MiniDumpWithUnloadedModules |
MiniDumpWithThreadInfo;
while (path.empty())
{
try
{
utf8Path = AuIOFS::NormalizePathRet("./Logs/Crashes/" + dumpName);
path = Locale::ConvertFromUTF8(utf8Path);
}
catch (...)
{
}
}
AuIOFS::CreateDirectories(utf8Path, true); // potentially unsafe / could throw inside
hFile = Win32Open(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, false, CREATE_ALWAYS, 0, FILE_ATTRIBUTE_NORMAL);
if (hFile != INVALID_HANDLE_VALUE)
{
AuLogWarn("[1] Couldn't open minidump file. Has a debugger locked the .dmp file?");
goto miniMiniDumpOut;
}
if (!pMiniDumpWriteDump)
{
goto miniMiniDumpOut;
}
ok = pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr);
if (!ok)
{
AuLogWarn("Couldn't write minidump: {:x}", GetLastError());
goto miniMiniDumpOut;
}
CloseHandle(hFile);
miniMiniDumpOut:
if (fatal)
{
Telemetry::NewBlackBoxEntryMinidump report {};
report.includesRx = false;
report.resource.path = dumpName;
report.resource.type = Telemetry::ENewBlackBoxResourceType::eLocal;
Telemetry::ReportDyingBreath(report);
Win32Terminate();
}
}
static void CacheInternalBuildSymbols()
{
#if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG)
if (pSymInitialize)
{
pSymInitialize(GetCurrentProcess(), NULL, TRUE);
}
#endif
}
static void DisableWindowsErrorReporting()
{
#if 0
// Windows has this annoying watchdog that triggers when your main loop doesnt respond after a while
// It's aggressive in its approach, giving the users a choice to forcefully terminate as soon as they spam click a busy app,
// or never at all. latterly, its not uncommon for the app to not come back up, bc win32.
// It's too easy to trigger the watchdog and impossible to stop it from deciding the windowed application must die
AuString procName;
if (!Process::GetProcName(procName))
{
AuLogWarn("Couldn't disable Microsoft Glowware Reporting!");
return;
}
// Yes, this is prone to module-name conflict issues because it's a dumb registry utility function
// This isn't what we want, but it's probably what the user wants
// MSFT is such a loving company, they'll do almost nothing to ensure they wont steal all your data
WerAddExcludedApplication(AuLocale::ConvertFromUTF8(procName).c_str(), false);
#else
// This implementation doesn't require a single IAT entry to a pointless dll
AuString procName;
if (!Process::GetProcName(procName))
{
return;
}
auto procNameWide = AuLocale::ConvertFromUTF8(procName);
HKEY hKey;
if (pRegOpenKeyExW &&
pRegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\Windows Error Reporting\\ExcludedApplications", 0, KEY_WRITE, &hKey) == ERROR_SUCCESS)
{
DWORD bioluminescenceReductionFactor { 1 };
if (pRegSetValueExW)
{
(void)pRegSetValueExW(hKey, procNameWide.c_str(), 0, REG_DWORD, (const BYTE *)&bioluminescenceReductionFactor, sizeof(DWORD));
}
if (pRegCloseKey)
{
pRegCloseKey(hKey);
}
}
#endif
}
void InitWin32()
{
// ...
if (gRuntimeConfig.debug.bNonshipPrecachesSymbols)
{
CacheInternalBuildSymbols();
}
// ...
DisableWindowsErrorReporting();
// ..
if (gRuntimeConfig.debug.bEnableWin32RootExceptionHandler)
{
AddVectoredExceptionHandler(1, HandleVectorException);
}
}
}