AuroraRuntime/Source/Debug/ExceptionWatcher.Win32.cpp

453 lines
14 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/ProcessMap.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/ProcessMap.hpp>
#include <Source/IO/FS/FS.hpp>
#include <Source/Grug/Grug.hpp>
#include <Source/Exit/Exit.hpp>
#include <WerApi.h>
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
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;
ParseStack(ExceptionInfo->ContextRecord, backtrace);
#if defined(_AU_USE_EXTENDED_FWD_FACING_DEBUGGING)
bool isInternal = true;
#else
bool isInternal = false;
#endif
if (pThrowInfo)
{
ReportSEH(handle, exception, pThrowInfo, handleNoCppObject, backtrace, [&](const AuString &str)
{
// Pre-submit callback -> its showtime
if ((isCritical || isInternal) && (minimal == 0))
{
if (gRuntimeConfig.debug.printExceptionStackTracesOut)
{
AuLogWarn("NT Exception: 0x{:x}, {}", ExceptionInfo->ExceptionRecord->ExceptionCode, str);
AuLogWarn("{}", StringifyStackTrace(backtrace));
}
}
});
}
try
{
Grug::Arrow empty;
Grug::HurlRaiseProblematicEvent(&empty);
if (isCritical)
{
Telemetry::Mayday();
}
if (isCritical || gRuntimeConfig.debug.isExceptionThrowFatal) // exception = literally anything
{
PlatformHandleFatal(true);
}
}
catch (...)
{
}
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 = ExceptionInfo != nullptr;
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 = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
{
AuLogWarn("Couldn't open minidump file. Has a debugger locked the .dmp file?");
goto miniDumpOut;
}
ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr);
if (!ok)
{
AuLogWarn("Couldn't write minidump");
goto miniDumpOut;
}
CloseHandle(hFile);
miniDumpOut:
try
{
AuDebug::PrintError();
}
catch (...)
{
}
if (isFatal)
{
__fastfail('fokd');
}
}
#endif
void BlackboxReport(_EXCEPTION_POINTERS *ExceptionInfo, bool fatal)
{
AuString path;
HANDLE hFile;
AuString utf8Path;
MINIDUMP_EXCEPTION_INFORMATION info;
auto ok = AuIOFS::GetProfileDomain(path); // < could throw inside
if (!ok)
{
path = ".\\";
}
info.ClientPointers = true;
info.ThreadId = GetCurrentThreadId();
info.ExceptionPointers = ExceptionInfo;
static const DWORD flags = MiniDumpWithDataSegs |
MiniDumpWithFullMemoryInfo |
MiniDumpWithHandleData |
MiniDumpWithUnloadedModules |
MiniDumpWithThreadInfo;
static std::wstring pathStorage(8192, L' ');
int index {};
index += MultiByteToWideChar(CP_UTF8, 0, path.c_str(), path.length(), pathStorage.data() + index, pathStorage.size() - index);
static const std::string crashesSlash = "Crashes\\";
index += MultiByteToWideChar(CP_UTF8, 0, crashesSlash.c_str(), crashesSlash.length(), pathStorage.data() + index, pathStorage.size() - index);
auto dumpName = GetDumpName();;
index += MultiByteToWideChar(CP_UTF8, 0, dumpName.c_str(), dumpName.length(), pathStorage.data() + index, pathStorage.size() - index);
pathStorage.resize(index);
AuIOFS::CreateDirectories(utf8Path, true); // potentially unsafe / could throw inside
hFile = CreateFileW(pathStorage.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile != INVALID_HANDLE_VALUE)
{
AuLogWarn("[1] Couldn't open minidump file. Has a debugger locked the .dmp file?");
goto miniMiniDumpOut;
}
ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, (MINIDUMP_TYPE)flags, &info, nullptr, nullptr);
if (!ok)
{
AuLogWarn("Couldn't write minidump");
goto miniMiniDumpOut;
}
CloseHandle(hFile);
miniMiniDumpOut:
Telemetry::NewBlackBoxEntryMinidump report {};
report.includesRx = false;
report.resource.path = dumpName; // <COPY
report.resource.type = Telemetry::ENewBlackBoxResourceType::eLocal;
Telemetry::ReportDyingBreath(report);
if (fatal)
{
__fastfail('fokd');
}
}
static void CacheInternalBuildSymbols()
{
#if defined(AU_CFG_ID_INTERNAL) || defined(AU_CFG_ID_DEBUG)
SymInitialize(GetCurrentProcess(), NULL, TRUE);
#endif
}
static void DisableWindowsErrorReporting()
{
// 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 to forcefully terminate as soon as they spam click a busy app,
// - or never, if the user decides its time for a coffee break the time an app goes grey
// 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);
}
void InitWin32()
{
// ...
if (gRuntimeConfig.debug.nonshipPrecachesSymbols)
{
CacheInternalBuildSymbols();
}
// ...
DisableWindowsErrorReporting();
// ..
if (gRuntimeConfig.debug.enableWin32RootExceptionHandler)
{
AddVectoredExceptionHandler(1, HandleVectorException);
}
}
}