diff --git a/BuildConfig.cpp.in b/BuildConfig.cpp.in index 1ea07374..76196b7e 100644 --- a/BuildConfig.cpp.in +++ b/BuildConfig.cpp.in @@ -29,7 +29,9 @@ Config::Config() UPDATER_FORCE_LOCAL = @MultiMC_UPDATER_FORCE_LOCAL_value@; GIT_COMMIT = "@MultiMC_GIT_COMMIT@"; + GIT_COMMIT_CSTR = "@MultiMC_GIT_COMMIT@"; VERSION_STR = "@MultiMC_VERSION_STRING@"; + VERSION_CSTR = "@MultiMC_VERSION_STRING@"; NEWS_RSS_URL = "@MultiMC_NEWS_RSS_URL@"; } diff --git a/BuildConfig.h b/BuildConfig.h index baf2ad6d..5e10beb0 100644 --- a/BuildConfig.h +++ b/BuildConfig.h @@ -61,10 +61,14 @@ public: /// The commit hash of this build QString GIT_COMMIT; + const char* GIT_COMMIT_CSTR; /// This is printed on start to standard output QString VERSION_STR; + /// Version string as a char string. Used by the crash handling system to avoid touching heap memory. + const char* VERSION_CSTR; + /** * This is used to fetch the news RSS feed. * It defaults in CMakeLists.txt to "http://multimc.org/rss.xml" diff --git a/CMakeLists.txt b/CMakeLists.txt index 17589f9a..531f7342 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,8 +75,29 @@ if(${BIGENDIAN}) endif(${BIGENDIAN}) -######## Set URLs ######## +######## Dark magic crash reports ######## +option(MultiMC_HANDLE_SEGV "Handle fatal crashes and generate crash reports." OFF) +set(CRASH_HANDLER_IMPL "") +message(STATUS "Crash dumps are ${MultiMC_HANDLE_SEGV}") +if (MultiMC_HANDLE_SEGV) + add_definitions(-DHANDLE_SEGV) + if (WIN32) + find_package(DbgHelp) + set(MultiMC_LINK_ADDITIONAL_LIBS "${MultiMC_LINK_ADDITIONAL_LIBS}dbghelp") + set(MultiMC_CRASH_HANDLER_EXTRA_H "WinBacktrace.h") + set(MultiMC_CRASH_HANDLER_EXTRA_CPP "WinBacktrace.cpp") + endif () +endif () +option(MultiMC_TEST_SEGV "Intentionally segfault sometimes to test crash handling." OFF) +if (MultiMC_TEST_SEGV) + # TODO: Make this a unit test instead. + message(WARNING "You have enabled crash handler testing. MULTIMC WILL INTENTIONALLY CRASH ITSELF. Crashes should occur on exit and when the new instance button is pressed.") + add_definitions(-DTEST_SEGV) +endif () + + +######## Set URLs ######## set(MultiMC_NEWS_RSS_URL "http://multimc.org/rss.xml" CACHE STRING "URL to fetch MultiMC's news RSS feed from.") @@ -250,6 +271,12 @@ SET(MULTIMC_SOURCES BuildConfig.h ${PROJECT_BINARY_DIR}/BuildConfig.cpp + # Crash handling + HandleCrash.h + HandleCrash.cpp + ${MultiMC_CRASH_HANDLER_EXTRA_H} # Extra platform specific stuff + ${MultiMC_CRASH_HANDLER_EXTRA_CPP} + # Logging logger/QsDebugOutput.cpp logger/QsDebugOutput.h diff --git a/HandleCrash.cpp b/HandleCrash.cpp new file mode 100644 index 00000000..ed319cf1 --- /dev/null +++ b/HandleCrash.cpp @@ -0,0 +1,376 @@ +/* Copyright 2014 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is the Unix implementation of MultiMC's crash handling system. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined Q_OS_UNIX +#include +#include +#elif defined Q_OS_WIN32 +#include +#include +#include +#endif + +#include "BuildConfig.h" + +#include "HandleCrash.h" + +// The maximum number of frames to include in the backtrace. +#define BT_SIZE 20 + + +#define DUMPF_NAME_FMT "mmc-crash-%X.bm" // Black magic? Bowel movement? Dump? +// 1234567890 1234 +// The maximum number of digits in a unix timestamp when encoded in hexadecimal is about 17. +// Our format string is ~14 characters long. +// The maximum length of the dump file's filename should be well over both of these. 42 is a good number. +#define DUMPF_NAME_LEN 42 + +// {{{ Platform hackery + +#if defined Q_OS_UNIX + +struct CrashData +{ + int signal = 0; +}; + +// This has to be declared here, after the CrashData struct, but before the function that uses it. +void handleCrash(CrashData); + +void handler(int sig) +{ + CrashData cData; + cData.signal = sig; + handleCrash(cData); +} + +#elif defined Q_OS_WIN32 + +// Struct for storing platform specific crash information. +// This gets passed into the generic handler, which will use +// it to access platform specific information. +struct CrashData +{ + EXCEPTION_RECORD* exceptionInfo; + CONTEXT* context; +}; + +void handleCrash(CrashData); + +LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* eInfo) +{ + CrashData cData; + cData.exceptionInfo = eInfo->ExceptionRecord; + cData.context = eInfo->ContextRecord; + handleCrash(cData); + return EXCEPTION_EXECUTE_HANDLER; +} + +#endif + +// }}} + +// {{{ Handling + +#ifdef Q_OS_WIN32 +// #ThanksMicrosoft +// Blame Microsoft for this atrocity. +void dprintf(int fd, const char* fmt...) +{ + va_list args; + va_start(args, fmt); + char buffer[10240]; + // Just sprintf to a really long string and hope it works... + // This is a hack, but I can't think of a better way to do it easily. + int len = vsnprintf(buffer, 10240, fmt, args); + printf(buffer, fmt, args); + write(fd, buffer, len); + va_end(args); +} +#endif + +void getVsnType(char* out); +void readFromTo(int from, int to); + +void dumpErrorInfo(int dumpFile, CrashData crash) +{ +#ifdef Q_OS_UNIX + // TODO: Moar unix + dprintf(dumpFile, "Signal: %d\n", crash.signal); +#elif defined Q_OS_WIN32 + EXCEPTION_RECORD* excInfo = crash.exceptionInfo; + + dprintf(dumpFile, "Exception Code: %d\n", excInfo->ExceptionCode); + dprintf(dumpFile, "Exception Address: 0x%0X\n", excInfo->ExceptionAddress); +#endif +} + +void dumpMiscInfo(int dumpFile) +{ + char vsnType[42]; // The version type. If it's more than 42 chars, the universe might implode... + + // Get MMC info. + getVsnType(vsnType); + + // Get MMC info. + getVsnType(vsnType); + + dprintf(dumpFile, "MultiMC Version: %s\n", BuildConfig.VERSION_CSTR); + dprintf(dumpFile, "MultiMC Version Type: %s\n", vsnType); +} + +void dumpBacktrace(int dumpFile, CrashData crash) +{ +#ifdef Q_OS_UNIX + // Variables for storing crash info. + void* trace[BT_SIZE]; // Backtrace frames + size_t size; // The backtrace size + + // Get the backtrace. + size = backtrace(trace, BT_SIZE); + + // Dump the backtrace + dprintf(dumpFile, "---- BEGIN BACKTRACE ----\n"); + backtrace_symbols_fd(trace, size, dumpFile); + dprintf(dumpFile, "---- END BACKTRACE ----\n"); +#elif defined Q_OS_WIN32 + dprintf(dumpFile, "---- BEGIN BACKTRACE ----\n"); + + StackFrame stack[BT_SIZE]; + size_t size; + + SYMBOL_INFO *symbol; + HANDLE process; + + size = getBacktrace(stack, BT_SIZE, *crash.context); + + // FIXME: Accessing heap memory is supposedly "dangerous", + // but I can't find another way of doing this. + + // Initialize + process = GetCurrentProcess(); + if (!SymInitialize(process, NULL, true)) + { + dprintf(dumpFile, "Failed to initialize symbol handler. Can't print stack trace.\n"); + dprintf(dumpFile, "Here's a list of addresses in the call stack instead:\n"); + for(int i = 0; i < size; i++) + { + dprintf(dumpFile, "0x%0X\n", (DWORD64)stack[i].address); + } + } else { + // Allocate memory... ._. + symbol = (SYMBOL_INFO *) calloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char), 1); + symbol->MaxNameLen = 255; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + + // Dump stacktrace + for(int i = 0; i < size; i++) + { + DWORD64 addr = (DWORD64)stack[i].address; + if (!SymFromAddr(process, (DWORD64)(addr), 0, symbol)) + dprintf(dumpFile, "?? - 0x%0X\n", addr); + else + dprintf(dumpFile, "%s - 0x%0X\n", symbol->Name, symbol->Address); + } + + free(symbol); + } + + dprintf(dumpFile, "---- END BACKTRACE ----\n"); +#endif +} + +void dumpSysInfo(int dumpFile) +{ +#ifdef Q_OS_UNIX + bool gotSysInfo = false; // True if system info check succeeded + utsname sysinfo; // System information + + // Dump system info + if (uname(&sysinfo) >= 0) + { + dprintf(dumpFile, "OS System: %s\n", sysinfo.sysname); + dprintf(dumpFile, "OS Machine: %s\n", sysinfo.machine); + dprintf(dumpFile, "OS Release: %s\n", sysinfo.release); + dprintf(dumpFile, "OS Version: %s\n", sysinfo.version); + } else { + dprintf(dumpFile, "OS System: Unknown Unix"); + } +#else + // TODO: Get more information here. + dprintf(dumpFile, "OS System: Windows"); +#endif +} + +void dumpLogs(int dumpFile) +{ + int otherFile; + + // Attempt to attach the log file if the logger was initialized. + dprintf(dumpFile, "---- BEGIN LOGS ----\n"); + if (loggerInitialized) + { + otherFile = open("MultiMC-0.log", O_RDONLY); + readFromTo(otherFile, dumpFile); + } else { + dprintf(dumpFile, "Logger not initialized.\n"); + } + dprintf(dumpFile, "---- END LOGS ----\n"); +} + +// The signal handler. If this function is called, it means shit has probably collided with some sort of device one might use to keep oneself cool. +// This is the generic part of the code that will be called after platform specific handling is finished. +void handleCrash(CrashData crash) +{ +#ifdef Q_OS_UNIX + fprintf(stderr, "Fatal error! Received signal %d\n", crash.signal); +#endif + + time_t unixTime = 0; // Unix timestamp. Used to give our crash dumps "unique" names. + + char dumpFileName[DUMPF_NAME_LEN]; // The name of the file we're dumping to. + int dumpFile; // File descriptor for our dump file. + + // Determine what our dump file should be called. + // We'll just call it "mmc-crash-.dump" + // First, check the time. + time(&unixTime); + + // Now we get to do some !!FUN!! hackery to ensure we don't use the stack when we convert + // the timestamp from an int to a string. To do this, we just allocate a fixed size array + // of chars on the stack, and sprintf into it. We know the timestamp won't ever be longer + // than a certain number of digits, so this should work just fine. + // sprintf doesn't support writing signed values as hex, so this breaks on negative timestamps. + // It really shouldn't matter, though... + sprintf(dumpFileName, DUMPF_NAME_FMT, unixTime); + + // Now, we need to open the file. + // Fail if it already exists. This should never happen. + dumpFile = open(dumpFileName, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + + if (dumpFile >= 0) + { + // If we opened the dump file successfully. + // Dump everything we can and GTFO. + fprintf(stderr, "Dumping crash report to %s\n", dumpFileName); + + // Dump misc info + dprintf(dumpFile, "Unix Time: %d\n", unixTime); + dumpErrorInfo(dumpFile, crash); + dumpMiscInfo(dumpFile); + + dprintf(dumpFile, "\n"); + + dumpSysInfo(dumpFile); + + dprintf(dumpFile, "\n"); + + dumpBacktrace(dumpFile, crash); + + dprintf(dumpFile, "\n"); + + // DIE DIE DIE! + exit(1); + } + else + { + fprintf(stderr, "Failed to open dump file %s to write crash info (ERRNO: %d)\n", dumpFileName, errno); + exit(2); + } +} + + +// Reads data from the file descriptor on the first argument into the second argument. +void readFromTo(int from, int to) +{ + char buffer[1024]; + size_t lastread = 1; + while (lastread > 0) + { + lastread = read(from, buffer, 1024); + if (lastread > 0) write(to, buffer, lastread); + } +} + +// Writes the current version type to the given char buffer. +void getVsnType(char* out) +{ + switch (BuildConfig.versionTypeEnum) + { + case Config::Release: + sprintf(out, "Release"); + break; + case Config::ReleaseCandidate: + sprintf(out, "ReleaseCandidate"); + break; + case Config::Development: + sprintf(out, "Development"); + break; + default: + sprintf(out, "Unknown"); + break; + } +} + +// }}} + +// {{{ Misc + +#if defined TEST_SEGV +// Causes a crash. For testing. +void testCrash() +{ + char* lol = (char*)MMC->settings().get(); + lol -= 8; + + // Throw shit at the fan. + for (int i = 0; i < 8; i++) + lol[i] = 'f'; +} +#endif + +// Initializes the Unix crash handler. +void initBlackMagic() +{ +#ifdef Q_OS_UNIX + // Register the handler. + signal(SIGSEGV, handler); + signal(SIGABRT, handler); +#elif defined Q_OS_WIN32 + // I hate Windows + SetUnhandledExceptionFilter(ExceptionFilter); +#endif + +#ifdef TEST_SEGV + testCrash(); +#endif +} + +// }}} + diff --git a/HandleCrash.h b/HandleCrash.h new file mode 100644 index 00000000..3b2e7d00 --- /dev/null +++ b/HandleCrash.h @@ -0,0 +1,18 @@ +// This is a simple header file for the crash handling system. It exposes only one method, +// initBlackMagic, which initializes the system, registering signal handlers, or doing +// whatever stupid things need to be done on Windows. +// The platform specific implementations for this system are in UnixCrash.cpp and +// WinCrash.cpp. + +#if defined Q_OS_WIN +#warning Crash handling is not yet implemented on Windows. +#elif defined Q_OS_UNIX +#else +#warning Crash handling is not supported on this platform. +#endif + +/** + * Initializes the crash handling system. + */ +void initBlackMagic(); + diff --git a/MultiMC.cpp b/MultiMC.cpp index 41c81b7b..bdebe181 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -340,8 +340,11 @@ void MultiMC::initLogger() logger.addDestination(m_debugDestination.get()); // log all the things logger.setLoggingLevel(QsLogging::TraceLevel); + loggerInitialized = true; } +bool loggerInitialized = false; + void MultiMC::initGlobalSettings() { m_settings.reset(new INISettingsObject("multimc.cfg", this)); diff --git a/MultiMC.h b/MultiMC.h index 00eb97f8..0fd60b7d 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -48,6 +48,9 @@ enum UpdateFlag Q_DECLARE_FLAGS(UpdateFlags, UpdateFlag); Q_DECLARE_OPERATORS_FOR_FLAGS(UpdateFlags); +// Global var used by the crash handling system to determine if a log file should be included in a crash report. +extern bool loggerInitialized; + class MultiMC : public QApplication { Q_OBJECT diff --git a/WinBacktrace.cpp b/WinBacktrace.cpp new file mode 100644 index 00000000..1ea079bf --- /dev/null +++ b/WinBacktrace.cpp @@ -0,0 +1,77 @@ +/* Copyright 2014 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// CAUTION: +// This file contains all manner of hackery and insanity. +// I will not be responsible for any loss of sanity due to reading this code. +// Here be dragons! + +#include "WinBacktrace.h" + +#include + +#ifndef __i386__ +#error WinBacktrace is only supported on x86 architectures. +#endif + +// We need to do some crazy shit to walk through the stack. +// Windows unwinds the stack when an exception is thrown, so we +// need to examine the EXCEPTION_POINTERS's CONTEXT. +size_t getBacktrace(StackFrame *stack, size_t size, CONTEXT ctx) +{ + // Written using information and a bit of pseudocode from + // http://www.eptacom.net/pubblicazioni/pub_eng/except.html + // This is probably one of the most horrifying things I've ever written. + + // This tracks whether the current EBP is valid. + // When an invalid EBP is encountered, we stop walking the stack. + bool validEBP = true; + DWORD ebp = ctx.Ebp; // The current EBP (Extended Base Pointer) + DWORD eip = ctx.Eip; + int i; + for (i = 0; i < size; i++) + { + if (ebp & 3) + validEBP = false; + // FIXME: This function is obsolete, according to MSDN. + else if (IsBadReadPtr((void*) ebp, 8)) + validEBP = false; + + if (!validEBP) break; + + // Find the caller. + // On the first iteration, the caller is whatever EIP points to. + // On successive iterations, the caller is the byte after EBP. + BYTE* caller = !i ? (BYTE*)eip : *((BYTE**) ebp + 1); + // The first ebp is the EBP from the CONTEXT. + // On successive iterations, the EBP is the DWORD that the previous EBP points to. + ebp = !i ? ebp : *(DWORD*)ebp; + + // Find the caller's module. + // We'll use VirtualQuery to get information about the caller's address. + MEMORY_BASIC_INFORMATION mbi; + VirtualQuery(caller, &mbi, sizeof(mbi)); + + // We can get the instance handle from the allocation base. + HINSTANCE hInst = (HINSTANCE)mbi.AllocationBase; + + // If the handle is 0, then the EBP is invalid. + if (hInst == 0) validEBP = false; + // Otherwise, dump info about the caller. + else stack[i].address = (void*)caller; + } + + return i; +} diff --git a/WinBacktrace.h b/WinBacktrace.h new file mode 100644 index 00000000..d1e6301a --- /dev/null +++ b/WinBacktrace.h @@ -0,0 +1,44 @@ +/* Copyright 2014 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#ifndef SF_STR_LEN +// The max length of all strings in the StackFrame struct. +// Because it must be stack allocated, this must be known at compile time. +// Stuff longer than this will be truncated. +// Defaults to 4096 (4kB) +#define SF_STR_LEN 4096 +#endif + +// Data structure for holding information about a stack frame. +// There's some more hackery in here so it can be allocated on the stack. +struct StackFrame +{ + // The address of this stack frame. + void* address; + + // The name of the function at this address. + char funcName[SF_STR_LEN]; +}; + +// This function walks through the given CONTEXT structure, extracting a +// backtrace from it. +// The backtrace will be put into the array given by the `stack` argument +// with a maximum length of `size`. +// This function returns the size of the backtrace retrieved. +size_t getBacktrace(StackFrame* stack, size_t size, CONTEXT ctx); diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index 554a58ec..5ba05b2a 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -709,6 +709,11 @@ void MainWindow::setCatBackground(bool enabled) void MainWindow::on_actionAddInstance_triggered() { +#ifdef TEST_SEGV + // For further testing stuff. + int v = *((int*)-1); +#endif + if (!MMC->minecraftlist()->isLoaded() && m_versionLoadTask && m_versionLoadTask->isRunning()) { diff --git a/main.cpp b/main.cpp index 181d7299..a79cc014 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,12 @@ #include "MultiMC.h" #include "gui/MainWindow.h" +// Crash handling +#ifdef HANDLE_SEGV +#include +#endif + + int main_gui(MultiMC &app) { // show main window @@ -23,6 +29,11 @@ int main(int argc, char *argv[]) Q_INIT_RESOURCE(multimc); Q_INIT_RESOURCE(backgrounds); +#ifdef HANDLE_SEGV + // Register signal handler for generating crash reports. + initBlackMagic(); +#endif + switch (app.status()) { case MultiMC::Initialized: