mirror of
				https://github.com/OpenMW/openmw.git
				synced 2025-11-04 01:26:39 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			324 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			324 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
#include "windows_crashmonitor.hpp"
 | 
						|
 | 
						|
#include <components/windows.hpp>
 | 
						|
#include <Psapi.h>
 | 
						|
 | 
						|
#include <DbgHelp.h>
 | 
						|
 | 
						|
#include <memory>
 | 
						|
#include <thread>
 | 
						|
 | 
						|
#include <SDL_messagebox.h>
 | 
						|
 | 
						|
#include "windows_crashcatcher.hpp"
 | 
						|
#include "windows_crashshm.hpp"
 | 
						|
#include <components/debug/debuglog.hpp>
 | 
						|
 | 
						|
namespace Crash
 | 
						|
{
 | 
						|
    std::unordered_map<HWINEVENTHOOK, CrashMonitor*> CrashMonitor::smEventHookOwners{};
 | 
						|
    
 | 
						|
    using IsHungAppWindowFn = BOOL(WINAPI*)(HWND hwnd);
 | 
						|
 | 
						|
    // Obtains the pointer to user32.IsHungAppWindow, this function may be removed in the future.
 | 
						|
    // See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-ishungappwindow
 | 
						|
    static IsHungAppWindowFn getIsHungAppWindow() noexcept
 | 
						|
    {
 | 
						|
        auto user32Handle = LoadLibraryA("user32.dll");
 | 
						|
        if (user32Handle == nullptr)
 | 
						|
            return nullptr;
 | 
						|
 | 
						|
        return reinterpret_cast<IsHungAppWindowFn>(GetProcAddress(user32Handle, "IsHungAppWindow"));
 | 
						|
    }
 | 
						|
 | 
						|
    static const IsHungAppWindowFn sIsHungAppWindow = getIsHungAppWindow();
 | 
						|
    
 | 
						|
    CrashMonitor::CrashMonitor(HANDLE shmHandle)
 | 
						|
        : mShmHandle(shmHandle)
 | 
						|
    {
 | 
						|
        mShm = reinterpret_cast<CrashSHM*>(MapViewOfFile(mShmHandle, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(CrashSHM)));
 | 
						|
        if (mShm == nullptr)
 | 
						|
            throw std::runtime_error("Failed to map crash monitor shared memory");
 | 
						|
 | 
						|
        // accessing SHM without lock is OK here, the parent waits for a signal before continuing
 | 
						|
 | 
						|
        mShmMutex = mShm->mStartup.mShmMutex;
 | 
						|
        mAppProcessHandle = mShm->mStartup.mAppProcessHandle;
 | 
						|
        mAppMainThreadId = mShm->mStartup.mAppMainThreadId;
 | 
						|
        mSignalAppEvent = mShm->mStartup.mSignalApp;
 | 
						|
        mSignalMonitorEvent = mShm->mStartup.mSignalMonitor;
 | 
						|
    }
 | 
						|
 | 
						|
    CrashMonitor::~CrashMonitor()
 | 
						|
    {
 | 
						|
        if (mShm)
 | 
						|
            UnmapViewOfFile(mShm);
 | 
						|
 | 
						|
        // the handles received from the app are duplicates, we must close them
 | 
						|
 | 
						|
        if (mShmHandle)
 | 
						|
            CloseHandle(mShmHandle);
 | 
						|
 | 
						|
        if (mShmMutex)
 | 
						|
            CloseHandle(mShmMutex);
 | 
						|
 | 
						|
        if (mSignalAppEvent)
 | 
						|
            CloseHandle(mSignalAppEvent);
 | 
						|
 | 
						|
        if (mSignalMonitorEvent)
 | 
						|
            CloseHandle(mSignalMonitorEvent);
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::shmLock()
 | 
						|
    {
 | 
						|
        if (WaitForSingleObject(mShmMutex, CrashCatcherTimeout) != WAIT_OBJECT_0)
 | 
						|
            throw std::runtime_error("SHM monitor lock timed out");
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::shmUnlock()
 | 
						|
    {
 | 
						|
        ReleaseMutex(mShmMutex);
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::signalApp() const
 | 
						|
    {
 | 
						|
        SetEvent(mSignalAppEvent);
 | 
						|
    }
 | 
						|
 | 
						|
    bool CrashMonitor::waitApp() const
 | 
						|
    {
 | 
						|
        return WaitForSingleObject(mSignalMonitorEvent, CrashCatcherTimeout) == WAIT_OBJECT_0;
 | 
						|
    }
 | 
						|
 | 
						|
    bool CrashMonitor::isAppAlive() const
 | 
						|
    {
 | 
						|
        DWORD code = 0;
 | 
						|
        GetExitCodeProcess(mAppProcessHandle, &code);
 | 
						|
        return code == STILL_ACTIVE;
 | 
						|
    }
 | 
						|
 | 
						|
    bool CrashMonitor::isAppFrozen()
 | 
						|
    {
 | 
						|
        MSG message;
 | 
						|
        // Allow the event hook callback to run
 | 
						|
        PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE);
 | 
						|
 | 
						|
        if (!mAppWindowHandle)
 | 
						|
        {
 | 
						|
            EnumWindows([](HWND handle, LPARAM param) -> BOOL {
 | 
						|
                CrashMonitor& crashMonitor = *(CrashMonitor*)param;
 | 
						|
                DWORD processId;
 | 
						|
                if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mAppMainThreadId && processId == GetProcessId(crashMonitor.mAppProcessHandle))
 | 
						|
                {
 | 
						|
                    if (GetWindow(handle, GW_OWNER) == 0)
 | 
						|
                    {
 | 
						|
                        crashMonitor.mAppWindowHandle = handle;
 | 
						|
                        return false;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                return true;
 | 
						|
                }, (LPARAM)this);
 | 
						|
            if (mAppWindowHandle)
 | 
						|
            {
 | 
						|
                DWORD processId;
 | 
						|
                GetWindowThreadProcessId(mAppWindowHandle, &processId);
 | 
						|
                HWINEVENTHOOK eventHookHandle = SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, nullptr,
 | 
						|
                    [](HWINEVENTHOOK hWinEventHook, DWORD event, HWND windowHandle, LONG objectId, LONG childId, DWORD eventThread, DWORD eventTime)
 | 
						|
                    {
 | 
						|
                        CrashMonitor& crashMonitor = *smEventHookOwners[hWinEventHook];
 | 
						|
                        if (event == EVENT_OBJECT_DESTROY && windowHandle == crashMonitor.mAppWindowHandle && objectId == OBJID_WINDOW && childId == INDEXID_CONTAINER)
 | 
						|
                        {
 | 
						|
                            crashMonitor.mAppWindowHandle = nullptr;
 | 
						|
                            smEventHookOwners.erase(hWinEventHook);
 | 
						|
                            UnhookWinEvent(hWinEventHook);
 | 
						|
                        }
 | 
						|
                    }, processId, mAppMainThreadId, WINEVENT_OUTOFCONTEXT);
 | 
						|
                smEventHookOwners[eventHookHandle] = this;
 | 
						|
            }
 | 
						|
            else
 | 
						|
                return false;
 | 
						|
        }
 | 
						|
        if (sIsHungAppWindow != nullptr)
 | 
						|
            return sIsHungAppWindow(mAppWindowHandle);
 | 
						|
        else
 | 
						|
        {
 | 
						|
            BOOL debuggerPresent;
 | 
						|
 | 
						|
            if (CheckRemoteDebuggerPresent(mAppProcessHandle, &debuggerPresent) && debuggerPresent)
 | 
						|
                return false;
 | 
						|
            if (SendMessageTimeoutA(mAppWindowHandle, WM_NULL, 0, 0, 0, 5000, nullptr) == 0)
 | 
						|
                return GetLastError() == ERROR_TIMEOUT;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::run()
 | 
						|
    {
 | 
						|
        try
 | 
						|
        {
 | 
						|
            // app waits for monitor start up, let it continue
 | 
						|
            signalApp();
 | 
						|
 | 
						|
            bool running = true;
 | 
						|
            bool frozen = false;
 | 
						|
            while (isAppAlive() && running && !mFreezeAbort)
 | 
						|
            {
 | 
						|
                if (isAppFrozen())
 | 
						|
                {
 | 
						|
                    if (!frozen)
 | 
						|
                    {
 | 
						|
                        showFreezeMessageBox();
 | 
						|
                        frozen = true;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else if (frozen)
 | 
						|
                {
 | 
						|
                    hideFreezeMessageBox();
 | 
						|
                    frozen = false;
 | 
						|
                }
 | 
						|
 | 
						|
                if (!mFreezeAbort && waitApp())
 | 
						|
                {
 | 
						|
                    shmLock();
 | 
						|
 | 
						|
                    switch (mShm->mEvent)
 | 
						|
                    {
 | 
						|
                    case CrashSHM::Event::None:
 | 
						|
                        break;
 | 
						|
                    case CrashSHM::Event::Crashed:
 | 
						|
                        handleCrash();
 | 
						|
                        running = false;
 | 
						|
                        break;
 | 
						|
                    case CrashSHM::Event::Shutdown:
 | 
						|
                        running = false;
 | 
						|
                        break;
 | 
						|
                    case CrashSHM::Event::Startup:
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
 | 
						|
                    shmUnlock();
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (frozen)
 | 
						|
                hideFreezeMessageBox();
 | 
						|
 | 
						|
            if (mFreezeAbort)
 | 
						|
            {
 | 
						|
                TerminateProcess(mAppProcessHandle, 0xDEAD);
 | 
						|
                std::string message = "OpenMW appears to have frozen.\nCrash log saved to '" + std::string(mShm->mStartup.mLogFilePath) + "'.\nPlease report this to https://gitlab.com/OpenMW/openmw/issues !";
 | 
						|
                SDL_ShowSimpleMessageBox(0, "Fatal Error", message.c_str(), nullptr);
 | 
						|
            }
 | 
						|
 | 
						|
        } 
 | 
						|
        catch (...)
 | 
						|
        {
 | 
						|
            Log(Debug::Error) << "Exception in crash monitor, exiting";
 | 
						|
        }
 | 
						|
        signalApp();
 | 
						|
    }
 | 
						|
 | 
						|
    static std::wstring utf8ToUtf16(const std::string& utf8)
 | 
						|
    {
 | 
						|
        const int nLenWide = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), nullptr, 0);
 | 
						|
 | 
						|
        std::wstring utf16;
 | 
						|
        utf16.resize(nLenWide);
 | 
						|
        if (MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), utf8.size(), utf16.data(), nLenWide) != nLenWide)
 | 
						|
            return {};
 | 
						|
 | 
						|
        return utf16;
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::handleCrash()
 | 
						|
    {
 | 
						|
        DWORD processId = GetProcessId(mAppProcessHandle);
 | 
						|
 | 
						|
        try
 | 
						|
        {
 | 
						|
            HMODULE dbghelp = LoadLibraryA("dbghelp.dll");
 | 
						|
            if (dbghelp == NULL)
 | 
						|
                return;
 | 
						|
    
 | 
						|
            using MiniDumpWirteDumpFn = BOOL (WINAPI*)(
 | 
						|
                HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
 | 
						|
                PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, PMINIDUMP_CALLBACK_INFORMATION CallbackParam
 | 
						|
            );
 | 
						|
    
 | 
						|
            MiniDumpWirteDumpFn miniDumpWriteDump = (MiniDumpWirteDumpFn)GetProcAddress(dbghelp, "MiniDumpWriteDump");
 | 
						|
            if (miniDumpWriteDump == NULL)
 | 
						|
                return;
 | 
						|
 | 
						|
            std::wstring utf16Path = utf8ToUtf16(mShm->mStartup.mLogFilePath);
 | 
						|
            if (utf16Path.empty())
 | 
						|
                return;
 | 
						|
 | 
						|
            if (utf16Path.length() > MAX_PATH)
 | 
						|
                utf16Path = LR"(\\?\)" + utf16Path;
 | 
						|
 | 
						|
            HANDLE hCrashLog = CreateFileW(utf16Path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
 | 
						|
            if (hCrashLog == NULL || hCrashLog == INVALID_HANDLE_VALUE)
 | 
						|
                return;
 | 
						|
            if (auto err = GetLastError(); err != ERROR_ALREADY_EXISTS && err != 0)
 | 
						|
                return;
 | 
						|
 | 
						|
            EXCEPTION_POINTERS exp;
 | 
						|
            exp.ContextRecord = &mShm->mCrashed.mContext;
 | 
						|
            exp.ExceptionRecord = &mShm->mCrashed.mExceptionRecord;
 | 
						|
            MINIDUMP_EXCEPTION_INFORMATION infos = {};
 | 
						|
            infos.ThreadId = mShm->mCrashed.mThreadId;
 | 
						|
            infos.ExceptionPointers = &exp;
 | 
						|
            infos.ClientPointers = FALSE;
 | 
						|
            MINIDUMP_TYPE type = (MINIDUMP_TYPE)(MiniDumpWithDataSegs | MiniDumpWithHandleData);
 | 
						|
            miniDumpWriteDump(mAppProcessHandle, processId, hCrashLog, type, &infos, 0, 0);
 | 
						|
        }
 | 
						|
        catch (const std::exception&e)
 | 
						|
        {
 | 
						|
            Log(Debug::Error) << "CrashMonitor: " << e.what();
 | 
						|
        }
 | 
						|
        catch (...)
 | 
						|
        {
 | 
						|
            Log(Debug::Error) << "CrashMonitor: unknown exception";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::showFreezeMessageBox()
 | 
						|
    {
 | 
						|
        std::thread messageBoxThread([&]() {
 | 
						|
            SDL_MessageBoxButtonData button = { SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Abort" };
 | 
						|
            SDL_MessageBoxData messageBoxData = {
 | 
						|
                SDL_MESSAGEBOX_ERROR,
 | 
						|
                nullptr,
 | 
						|
                "OpenMW appears to have frozen",
 | 
						|
                "OpenMW appears to have frozen. Press Abort to terminate it and generate a crash dump.\nIf OpenMW hasn't actually frozen, this message box will disappear a within a few seconds of it becoming responsive.",
 | 
						|
                1,
 | 
						|
                &button,
 | 
						|
                nullptr
 | 
						|
            };
 | 
						|
 | 
						|
            int buttonId;
 | 
						|
            if (SDL_ShowMessageBox(&messageBoxData, &buttonId) == 0 && buttonId == 0)
 | 
						|
                mFreezeAbort = true;
 | 
						|
            });
 | 
						|
 | 
						|
        mFreezeMessageBoxThreadId = GetThreadId(messageBoxThread.native_handle());
 | 
						|
        messageBoxThread.detach();
 | 
						|
    }
 | 
						|
 | 
						|
    void CrashMonitor::hideFreezeMessageBox()
 | 
						|
    {
 | 
						|
        if (!mFreezeMessageBoxThreadId)
 | 
						|
            return;
 | 
						|
 | 
						|
        EnumWindows([](HWND handle, LPARAM param) -> BOOL {
 | 
						|
            CrashMonitor& crashMonitor = *(CrashMonitor*)param;
 | 
						|
            DWORD processId;
 | 
						|
            if (GetWindowThreadProcessId(handle, &processId) == crashMonitor.mFreezeMessageBoxThreadId && processId == GetCurrentProcessId())
 | 
						|
                PostMessage(handle, WM_CLOSE, 0, 0);
 | 
						|
            return true;
 | 
						|
            }, (LPARAM)this);
 | 
						|
 | 
						|
        mFreezeMessageBoxThreadId = 0;
 | 
						|
    }
 | 
						|
 | 
						|
} // namespace Crash
 |