453 lines
14 KiB
C++
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/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>
|
|
|
|
#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.bPrintExceptionStackTracesOut)
|
|
{
|
|
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.bIsExceptionThrowFatal) // 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 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);
|
|
}
|
|
|
|
void InitWin32()
|
|
{
|
|
// ...
|
|
if (gRuntimeConfig.debug.bNonshipPrecachesSymbols)
|
|
{
|
|
CacheInternalBuildSymbols();
|
|
}
|
|
|
|
// ...
|
|
DisableWindowsErrorReporting();
|
|
|
|
// ..
|
|
if (gRuntimeConfig.debug.bEnableWin32RootExceptionHandler)
|
|
{
|
|
AddVectoredExceptionHandler(1, HandleVectorException);
|
|
}
|
|
}
|
|
} |