/* * ExceptionHandler.cpp * -------------------- * Purpose: Code for handling crashes (unhandled exceptions) in OpenMPT. * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Mainfrm.h" #include "Mptrack.h" #include "AboutDialog.h" #include "InputHandler.h" #include "openmpt/sounddevice/SoundDevice.hpp" #include "Moddoc.h" #include #include "ExceptionHandler.h" #include "../misc/WriteMemoryDump.h" #include "../common/version.h" #include "../common/mptFileIO.h" #include "../soundlib/mod_specifications.h" #include OPENMPT_NAMESPACE_BEGIN // Write full memory dump instead of minidump. bool ExceptionHandler::fullMemDump = false; bool ExceptionHandler::stopSoundDeviceOnCrash = true; bool ExceptionHandler::stopSoundDeviceBeforeDump = false; // Delegate to system-specific crash processing once our own crash handler is // finished. This is useful to allow attaching a debugger. bool ExceptionHandler::delegateToWindowsHandler = false; // Allow debugging the unhandled exception filter. Normally, if a debugger is // attached, no exceptions are unhandled because the debugger handles them. If // debugExceptionHandler is true, an additional __try/__catch is inserted around // InitInstance(), ExitInstance() and the main message loop, which will call our // filter, which then can be stepped through in a debugger. bool ExceptionHandler::debugExceptionHandler = false; bool ExceptionHandler::useAnyCrashHandler = false; bool ExceptionHandler::useImplicitFallbackSEH = false; bool ExceptionHandler::useExplicitSEH = false; bool ExceptionHandler::handleStdTerminate = false; bool ExceptionHandler::handleMfcExceptions = false; static thread_local ExceptionHandler::Context *g_Context = nullptr; static LPTOP_LEVEL_EXCEPTION_FILTER g_OriginalUnhandledExceptionFilter = nullptr; static std::terminate_handler g_OriginalTerminateHandler = nullptr; static UINT g_OriginalErrorMode = 0; ExceptionHandler::Context *ExceptionHandler::SetContext(Context *newContext) noexcept { Context *oldContext = g_Context; g_Context = newContext; return oldContext; } static std::atomic & g_CrashCount() { static std::atomic s_CrashCount(0); return s_CrashCount; } static std::atomic & g_TaintCountDriver() { static std::atomic s_TaintCountDriver(0); return s_TaintCountDriver; } static std::atomic & g_TaintCountPlugin() { static std::atomic s_TaintCountPlugin(0); return s_TaintCountPlugin; } void ExceptionHandler::TaintProcess(ExceptionHandler::TaintReason reason) { switch(reason) { case ExceptionHandler::TaintReason::Driver: g_TaintCountDriver().fetch_add(1); break; case ExceptionHandler::TaintReason::Plugin: g_TaintCountPlugin().fetch_add(1); break; default: MPT_ASSERT_NOTREACHED(); break; } } enum DumpMode { DumpModeCrash = 0, // crash DumpModeWarning = 1, // assert DumpModeDebug = 2, // debug output (e.g. trace log) }; struct CrashOutputDirectory { bool valid; mpt::PathString path; CrashOutputDirectory() : valid(true) { const mpt::PathString timestampDir = mpt::PathString::FromCString((CTime::GetCurrentTime()).Format(_T("%Y-%m-%d %H.%M.%S\\"))); // Create a crash directory path = mpt::GetTempDirectory() + P_("OpenMPT Crash Files\\"); if(!path.IsDirectory()) { CreateDirectory(path.AsNative().c_str(), nullptr); } // Set compressed attribute in order to save disk space. // Debugging information should clutter the users computer as little as possible. // Performance is not important here. // Ignore any errors. SetFilesystemCompression(path); // Compression will be inherited by children directories and files automatically. path += timestampDir; if(!path.IsDirectory()) { if(!CreateDirectory(path.AsNative().c_str(), nullptr)) { valid = false; } } } }; class DebugReporter { private: int crashCount; int taintCountDriver; int taintCountPlugin; bool stateFrozen; const DumpMode mode; const CrashOutputDirectory crashDirectory; bool writtenMiniDump; bool writtenTraceLog; int rescuedFiles; private: static bool FreezeState(DumpMode mode); static bool Cleanup(DumpMode mode); bool GenerateDump(_EXCEPTION_POINTERS *pExceptionInfo); bool GenerateTraceLog(); int RescueFiles(); bool HasWrittenDebug() const { return writtenMiniDump || writtenTraceLog; } static void StopSoundDevice(); public: DebugReporter(DumpMode mode, _EXCEPTION_POINTERS *pExceptionInfo); ~DebugReporter(); void ReportError(mpt::ustring errorMessage); }; DebugReporter::DebugReporter(DumpMode mode, _EXCEPTION_POINTERS *pExceptionInfo) : crashCount(g_CrashCount().fetch_add(1) + 1) , taintCountDriver(g_TaintCountDriver().load()) , taintCountPlugin(g_TaintCountPlugin().load()) , stateFrozen(FreezeState(mode)) , mode(mode) , writtenMiniDump(false) , writtenTraceLog(false) , rescuedFiles(0) { if(mode == DumpModeCrash || mode == DumpModeWarning) { writtenMiniDump = GenerateDump(pExceptionInfo); } if(mode == DumpModeCrash || mode == DumpModeWarning || mode == DumpModeDebug) { writtenTraceLog = GenerateTraceLog(); } if(mode == DumpModeCrash || mode == DumpModeWarning) { rescuedFiles = RescueFiles(); } } DebugReporter::~DebugReporter() { Cleanup(mode); } bool DebugReporter::GenerateDump(_EXCEPTION_POINTERS *pExceptionInfo) { return WriteMemoryDump(pExceptionInfo, (crashDirectory.path + P_("crash.dmp")).AsNative().c_str(), ExceptionHandler::fullMemDump); } bool DebugReporter::GenerateTraceLog() { return mpt::log::Trace::Dump(crashDirectory.path + P_("trace.log")); } static void SaveDocumentSafe(CModDoc *pModDoc, const mpt::PathString &filename) { __try { pModDoc->OnSaveDocument(filename); } __except(EXCEPTION_EXECUTE_HANDLER) { // nothing } } // Rescue modified files... int DebugReporter::RescueFiles() { int numFiles = 0; auto documents = theApp.GetOpenDocuments(); for(auto modDoc : documents) { if(modDoc->IsModified()) { if(numFiles == 0) { // Show the rescue directory in Explorer... CTrackApp::OpenDirectory(crashDirectory.path); } mpt::PathString filename; filename += crashDirectory.path; filename += mpt::PathString::FromUnicode(mpt::ufmt::val(++numFiles)); filename += P_("_"); filename += mpt::PathString::FromCString(modDoc->GetTitle()).SanitizeComponent(); filename += P_("."); filename += mpt::PathString::FromUTF8(modDoc->GetSoundFile().GetModSpecifications().fileExtension); try { SaveDocumentSafe(modDoc, filename); } catch(...) { continue; } } } return numFiles; } void DebugReporter::ReportError(mpt::ustring errorMessage) { if(!crashDirectory.valid) { errorMessage += UL_("\n\n"); errorMessage += UL_("Could not create the following directory for saving debug information and modified files to:\n"); errorMessage += crashDirectory.path.ToUnicode(); } if(HasWrittenDebug()) { errorMessage += UL_("\n\n"); errorMessage += UL_("Debug information has been saved to\n"); errorMessage += crashDirectory.path.ToUnicode(); } if(rescuedFiles > 0) { errorMessage += UL_("\n\n"); if(rescuedFiles == 1) { errorMessage += UL_("1 modified file has been rescued, but it cannot be guaranteed that it is still intact."); } else { errorMessage += MPT_UFORMAT("{} modified files have been rescued, but it cannot be guaranteed that they are still intact.")(rescuedFiles); } } errorMessage += UL_("\n\n"); errorMessage += MPT_UFORMAT("OpenMPT {} {} ({} ({}))") ( Build::GetVersionStringExtended() , mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()) , SourceInfo::Current().GetUrlWithRevision() , SourceInfo::Current().GetStateString() ); errorMessage += UL_("\n\n"); errorMessage += MPT_UFORMAT("Session error count: {}\n")(crashCount); if(taintCountDriver > 0 || taintCountPlugin > 0) { errorMessage += UL_("Process is in tainted state!\n"); errorMessage += MPT_UFORMAT("Previously masked driver crashes: {}\n")(taintCountDriver); errorMessage += MPT_UFORMAT("Previously masked plugin crashes: {}\n")(taintCountPlugin); } errorMessage += UL_("\n"); { mpt::SafeOutputFile sf(crashDirectory.path + P_("error.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& f = sf; f.imbue(std::locale::classic()); f << mpt::replace(mpt::ToCharset(mpt::Charset::UTF8, errorMessage), std::string("\n"), std::string("\r\n")); } if(auto ih = CMainFrame::GetInputHandler(); ih != nullptr) { mpt::SafeOutputFile sf(crashDirectory.path + P_("last-commands.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream &f = sf; f.imbue(std::locale::classic()); const auto commandSet = ih->m_activeCommandSet.get(); f << "Last commands:\n"; for(size_t i = 0; i < ih->m_lastCommands.size(); i++) { CommandID id = ih->m_lastCommands[(ih->m_lastCommandPos + i) % ih->m_lastCommands.size()]; if(id == kcNull) continue; f << mpt::afmt::val(id); if(commandSet) f << " (" << mpt::ToCharset(mpt::Charset::UTF8, commandSet->GetCommandText(id)) << ")"; f << "\n"; } } { mpt::SafeOutputFile sf(crashDirectory.path + P_("threads.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& f = sf; f.imbue(std::locale::classic()); f << MPT_AFORMAT("current : {}")(mpt::afmt::hex0<8>(GetCurrentThreadId())) << "\r\n"; f << MPT_AFORMAT("GUI : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindGUI))) << "\r\n"; f << MPT_AFORMAT("Audio : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindAudio))) << "\r\n"; f << MPT_AFORMAT("Notify : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindNotify))) << "\r\n"; f << MPT_AFORMAT("WatchDir: {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindWatchdir))) << "\r\n"; } static constexpr struct { const mpt::uchar * section; const mpt::uchar * key; } configAnonymize[] = { { UL_("Version"), UL_("InstallGUID") }, { UL_("Recent File List"), nullptr }, }; { mpt::SafeOutputFile sf(crashDirectory.path + P_("active-settings.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& f = sf; f.imbue(std::locale::classic()); if(theApp.GetpSettings()) { SettingsContainer &settings = theApp.GetSettings(); for(const auto &it : settings) { bool skipPath = false; for(const auto &path : configAnonymize) { if((path.key == nullptr && path.section == it.first.GetRefSection()) // Omit entire section || (path.key != nullptr && it.first == SettingPath(path.section, path.key))) // Omit specific key { skipPath = true; } } if(skipPath) { continue; } f << mpt::ToCharset(mpt::Charset::UTF8, it.first.FormatAsString() + U_(" = ") + it.second.GetRefValue().FormatValueAsString()) << std::endl; } } } { const mpt::PathString crashStoredSettingsFilename = crashDirectory.path + P_("stored-mptrack.ini"); CopyFile ( theApp.GetConfigFileName().AsNative().c_str() , crashStoredSettingsFilename.AsNative().c_str() , FALSE ); IniFileSettingsContainer crashStoredSettings{crashStoredSettingsFilename}; for(const auto &path : configAnonymize) { if(path.key) { crashStoredSettings.Write(SettingPath(path.section, path.key), SettingValue(mpt::ustring())); } else { crashStoredSettings.Remove(path.section); } } crashStoredSettings.Flush(); } /* // This is very slow, we instead write active-settings.txt above. { IniFileSettingsBackend f(crashDirectory.path + P_("active-mptrack.ini")); if(theApp.GetpSettings()) { SettingsContainer & settings = theApp.GetSettings(); for(const auto &it : settings) { f.WriteSetting(it.first, it.second.GetRefValue()); } } } */ { mpt::SafeOutputFile sf(crashDirectory.path + P_("about-openmpt.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& f = sf; f.imbue(std::locale::classic()); f << mpt::ToCharset(mpt::Charset::UTF8, CAboutDlg::GetTabText(0)); } { mpt::SafeOutputFile sf(crashDirectory.path + P_("about-components.txt"), std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& f = sf; f.imbue(std::locale::classic()); f << mpt::ToCharset(mpt::Charset::UTF8, CAboutDlg::GetTabText(1)); } Reporting::Error(errorMessage, (mode == DumpModeWarning) ? "OpenMPT Warning" : "OpenMPT Crash", CMainFrame::GetMainFrame()); } // Freezes the state as much as possible in order to avoid further confusion by // other (possibly still running) threads bool DebugReporter::FreezeState(DumpMode mode) { MPT_TRACE(); // seal the trace log as early as possible mpt::log::Trace::Seal(); if(mode == DumpModeCrash || mode == DumpModeWarning) { if(CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->gpSoundDevice && CMainFrame::GetMainFrame()->gpSoundDevice->DebugIsFragileDevice()) { // For fragile devices, always stop the device. Stop before the dumping if not in realtime context. if(!CMainFrame::GetMainFrame()->gpSoundDevice->DebugInRealtimeCallback()) { StopSoundDevice(); } } else { if(ExceptionHandler::stopSoundDeviceOnCrash && ExceptionHandler::stopSoundDeviceBeforeDump) { StopSoundDevice(); } } } return true; } static void StopSoundDeviceSafe(CMainFrame *pMainFrame) { __try { if(pMainFrame->gpSoundDevice) { pMainFrame->gpSoundDevice->Close(); } if(pMainFrame->m_NotifyTimer) { pMainFrame->KillTimer(pMainFrame->m_NotifyTimer); pMainFrame->m_NotifyTimer = 0; } } __except(EXCEPTION_EXECUTE_HANDLER) { // nothing } } void DebugReporter::StopSoundDevice() { CMainFrame* pMainFrame = CMainFrame::GetMainFrame(); if(pMainFrame) { try { StopSoundDeviceSafe(pMainFrame); } catch(...) { // nothing } } } bool DebugReporter::Cleanup(DumpMode mode) { MPT_TRACE(); if(mode == DumpModeCrash || mode == DumpModeWarning) { if(CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->gpSoundDevice && CMainFrame::GetMainFrame()->gpSoundDevice->DebugIsFragileDevice()) { // For fragile devices, always stop the device. Stop after the dumping if in realtime context. if(CMainFrame::GetMainFrame()->gpSoundDevice->DebugInRealtimeCallback()) { StopSoundDevice(); } } else { if(ExceptionHandler::stopSoundDeviceOnCrash && !ExceptionHandler::stopSoundDeviceBeforeDump) { StopSoundDevice(); } } } return true; } // Different entry points for different situations in which we want to dump some information static bool IsCxxException(_EXCEPTION_POINTERS *pExceptionInfo) { if (!pExceptionInfo) return false; if (!pExceptionInfo->ExceptionRecord) return false; if (pExceptionInfo->ExceptionRecord->ExceptionCode != 0xE06D7363u) return false; return true; } template static const E * GetCxxException(_EXCEPTION_POINTERS *pExceptionInfo) { // https://blogs.msdn.microsoft.com/oldnewthing/20100730-00/?p=13273 struct info_a_t { DWORD bitmask; // Probably: 1=Const, 2=Volatile DWORD destructor; // RVA (Relative Virtual Address) of destructor for that exception object DWORD unknown; DWORD catchableTypesPtr; // RVA of instance of type "B" }; struct info_c_t { DWORD someBitmask; DWORD typeInfo; // RVA of std::type_info for that type DWORD memberDisplacement; // Add to ExceptionInformation[1] in EXCEPTION_RECORD to obtain 'this' pointer. DWORD virtBaseRelated1; // -1 if no virtual base DWORD virtBaseRelated2; // ? DWORD objectSize; // Size of the object in bytes DWORD probablyCopyCtr; // RVA of copy constructor (?) }; if(!pExceptionInfo) return nullptr; if (!pExceptionInfo->ExceptionRecord) return nullptr; if(pExceptionInfo->ExceptionRecord->ExceptionCode != 0xE06D7363u) return nullptr; #ifdef _WIN64 if(pExceptionInfo->ExceptionRecord->NumberParameters != 4) return nullptr; #else if(pExceptionInfo->ExceptionRecord->NumberParameters != 3) return nullptr; #endif if(pExceptionInfo->ExceptionRecord->ExceptionInformation[0] != 0x19930520u) return nullptr; std::uintptr_t base_address = 0; #ifdef _WIN64 base_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[3]; #else base_address = 0; #endif std::uintptr_t obj_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[1]; if(!obj_address) return nullptr; std::uintptr_t info_a_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[2]; if(!info_a_address) return nullptr; const info_a_t * info_a = reinterpret_cast(info_a_address); std::uintptr_t info_b_offset = info_a->catchableTypesPtr; if(!info_b_offset) return nullptr; const DWORD * info_b = reinterpret_cast(base_address + info_b_offset); for(DWORD type = 1; type <= info_b[0]; ++type) { std::uintptr_t info_c_offset = info_b[type]; if(!info_c_offset) continue; const info_c_t * info_c = reinterpret_cast(base_address + info_c_offset); if(!info_c->typeInfo) continue; const std::type_info * ti = reinterpret_cast(base_address + info_c->typeInfo); if(*ti != typeid(E)) continue; const E * e = reinterpret_cast(obj_address + info_c->memberDisplacement); return e; } return nullptr; } void ExceptionHandler::UnhandledMFCException(CException * e, const MSG * pMsg) { DebugReporter report(DumpModeCrash, nullptr); mpt::ustring errorMessage; if(e && dynamic_cast(e)) { TCHAR tmp[1024 + 1]; MemsetZero(tmp); if(dynamic_cast(e)->GetErrorMessage(tmp, static_cast(std::size(tmp) - 1)) != 0) { tmp[1024] = 0; errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}': {}.") (mpt::ufmt::dec(pMsg ? pMsg->message : 0) , mpt::ToUnicode(CString(tmp)) ); } else { errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}': {}.") (mpt::ufmt::dec(pMsg ? pMsg->message : 0) , mpt::ToUnicode(CString(tmp)) ); } } else { errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}'.") ( mpt::ufmt::dec(pMsg ? pMsg->message : 0) ); } report.ReportError(errorMessage); } static void UnhandledExceptionFilterImpl(_EXCEPTION_POINTERS *pExceptionInfo) { DebugReporter report(DumpModeCrash, pExceptionInfo); mpt::ustring errorMessage; const std::exception * pE = GetCxxException(pExceptionInfo); if(g_Context) { if(!g_Context->description.empty()) { errorMessage += MPT_UFORMAT("OpenMPT detected a crash in '{}'.\nThis is very likely not an OpenMPT bug. Please report the problem to the respective software author.\n")(g_Context->description); } else { errorMessage += MPT_UFORMAT("OpenMPT detected a crash in unknown foreign code.\nThis is likely not an OpenMPT bug.\n")(); } } if(pE) { const std::exception & e = *pE; errorMessage += MPT_UFORMAT("Unhandled C++ exception '{}' occurred at address 0x{}: '{}'.") ( mpt::ToUnicode(mpt::Charset::ASCII, typeid(e).name()) , mpt::ufmt::hex0(reinterpret_cast(pExceptionInfo->ExceptionRecord->ExceptionAddress)) , mpt::get_exception_text(e) ); } else { errorMessage += MPT_UFORMAT("Unhandled exception 0x{} at address 0x{} occurred.") ( mpt::ufmt::HEX0<8>(pExceptionInfo->ExceptionRecord->ExceptionCode) , mpt::ufmt::hex0(reinterpret_cast(pExceptionInfo->ExceptionRecord->ExceptionAddress)) ); } report.ReportError(errorMessage); } LONG ExceptionHandler::UnhandledExceptionFilterContinue(_EXCEPTION_POINTERS *pExceptionInfo) { UnhandledExceptionFilterImpl(pExceptionInfo); // Disable the call to std::terminate() as that would re-renter the crash // handler another time, but with less information available. #if 0 // MSVC implements calling std::terminate by its own UnhandledExeptionFilter. // However, we do overwrite it here, thus we have to call std::terminate // ourselves. if (IsCxxException(pExceptionInfo)) { std::terminate(); } #endif // Let a potential debugger handle the exception... return EXCEPTION_CONTINUE_SEARCH; } LONG ExceptionHandler::ExceptionFilter(_EXCEPTION_POINTERS *pExceptionInfo) { UnhandledExceptionFilterImpl(pExceptionInfo); // Let a potential debugger handle the exception... return EXCEPTION_EXECUTE_HANDLER; } static void mpt_unexpected_handler(); static void mpt_terminate_handler(); void ExceptionHandler::Register() { if(useImplicitFallbackSEH) { g_OriginalUnhandledExceptionFilter = ::SetUnhandledExceptionFilter(&UnhandledExceptionFilterContinue); } if(handleStdTerminate) { g_OriginalTerminateHandler = std::set_terminate(&mpt_terminate_handler); } } void ExceptionHandler::ConfigureSystemHandler() { #if (_WIN32_WINNT >= 0x0600) if(delegateToWindowsHandler) { //SetErrorMode(0); g_OriginalErrorMode = ::GetErrorMode(); } else { g_OriginalErrorMode = ::SetErrorMode(::GetErrorMode() | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); } #else // _WIN32_WINNT < 0x0600 if(delegateToWindowsHandler) { g_OriginalErrorMode = ::SetErrorMode(0); } else { g_OriginalErrorMode = ::SetErrorMode(::SetErrorMode(0) | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); } #endif // _WIN32_WINNT } void ExceptionHandler::UnconfigureSystemHandler() { ::SetErrorMode(g_OriginalErrorMode); g_OriginalErrorMode = 0; } void ExceptionHandler::Unregister() { if(handleStdTerminate) { std::set_terminate(g_OriginalTerminateHandler); g_OriginalTerminateHandler = nullptr; } if(useImplicitFallbackSEH) { ::SetUnhandledExceptionFilter(g_OriginalUnhandledExceptionFilter); g_OriginalUnhandledExceptionFilter = nullptr; } } static void mpt_terminate_handler() { DebugReporter(DumpModeCrash, nullptr).ReportError(U_("A C++ runtime crash occurred: std::terminate() called.")); #if 1 std::abort(); #else if(g_OriginalTerminateHandler) { g_OriginalTerminateHandler(); } else { std::abort(); } #endif } #if defined(MPT_ASSERT_HANDLER_NEEDED) MPT_NOINLINE void AssertHandler(const mpt::source_location &loc, const char *expr, const char *msg) { DebugReporter report(msg ? DumpModeWarning : DumpModeCrash, nullptr); if(IsDebuggerPresent()) { OutputDebugString(_T("ASSERT(")); OutputDebugString(mpt::ToWin(mpt::Charset::ASCII, expr).c_str()); OutputDebugString(_T(") failed\n")); DebugBreak(); } else { mpt::ustring errorMessage; if(msg) { errorMessage = MPT_UFORMAT("Internal state inconsistency detected at {}({}). This is just a warning that could potentially lead to a crash later on: {} [{}].") ( mpt::ToUnicode(mpt::Charset::ASCII, loc.file_name() ? loc.file_name() : "") , loc.line() , mpt::ToUnicode(mpt::Charset::ASCII, msg) , mpt::ToUnicode(mpt::Charset::ASCII, loc.function_name() ? loc.function_name() : "") ); } else { errorMessage = MPT_UFORMAT("Internal error occurred at {}({}): ASSERT({}) failed in [{}].") ( mpt::ToUnicode(mpt::Charset::ASCII, loc.file_name() ? loc.file_name() : "") , loc.line() , mpt::ToUnicode(mpt::Charset::ASCII, expr) , mpt::ToUnicode(mpt::Charset::ASCII, loc.function_name() ? loc.function_name() : "") ); } report.ReportError(errorMessage); } } #endif // MPT_ASSERT_HANDLER_NEEDED void DebugInjectCrash() { DebugReporter(DumpModeCrash, nullptr).ReportError(U_("Injected crash.")); } void DebugTraceDump() { DebugReporter report(DumpModeDebug, nullptr); } OPENMPT_NAMESPACE_END