Merge branch 'feature_crashreport' into develop

This commit is contained in:
Jan Dalheimer 2014-05-21 15:57:34 +02:00
commit fc3c0b0971
11 changed files with 571 additions and 1 deletions

View File

@ -29,7 +29,9 @@ Config::Config()
UPDATER_FORCE_LOCAL = @MultiMC_UPDATER_FORCE_LOCAL_value@; UPDATER_FORCE_LOCAL = @MultiMC_UPDATER_FORCE_LOCAL_value@;
GIT_COMMIT = "@MultiMC_GIT_COMMIT@"; GIT_COMMIT = "@MultiMC_GIT_COMMIT@";
GIT_COMMIT_CSTR = "@MultiMC_GIT_COMMIT@";
VERSION_STR = "@MultiMC_VERSION_STRING@"; VERSION_STR = "@MultiMC_VERSION_STRING@";
VERSION_CSTR = "@MultiMC_VERSION_STRING@";
NEWS_RSS_URL = "@MultiMC_NEWS_RSS_URL@"; NEWS_RSS_URL = "@MultiMC_NEWS_RSS_URL@";
} }

View File

@ -61,10 +61,14 @@ public:
/// The commit hash of this build /// The commit hash of this build
QString GIT_COMMIT; QString GIT_COMMIT;
const char* GIT_COMMIT_CSTR;
/// This is printed on start to standard output /// This is printed on start to standard output
QString VERSION_STR; 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. * This is used to fetch the news RSS feed.
* It defaults in CMakeLists.txt to "http://multimc.org/rss.xml" * It defaults in CMakeLists.txt to "http://multimc.org/rss.xml"

View File

@ -75,8 +75,29 @@ if(${BIGENDIAN})
endif(${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.") 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 BuildConfig.h
${PROJECT_BINARY_DIR}/BuildConfig.cpp ${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 # Logging
logger/QsDebugOutput.cpp logger/QsDebugOutput.cpp
logger/QsDebugOutput.h logger/QsDebugOutput.h

376
HandleCrash.cpp Normal file
View File

@ -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 <stdio.h>
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <MultiMC.h>
#if defined Q_OS_UNIX
#include <sys/utsname.h>
#include <execinfo.h>
#elif defined Q_OS_WIN32
#include <windows.h>
#include <dbghelp.h>
#include <WinBacktrace.h>
#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-<unixtime>.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
}
// }}}

18
HandleCrash.h Normal file
View File

@ -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();

View File

@ -340,8 +340,11 @@ void MultiMC::initLogger()
logger.addDestination(m_debugDestination.get()); logger.addDestination(m_debugDestination.get());
// log all the things // log all the things
logger.setLoggingLevel(QsLogging::TraceLevel); logger.setLoggingLevel(QsLogging::TraceLevel);
loggerInitialized = true;
} }
bool loggerInitialized = false;
void MultiMC::initGlobalSettings() void MultiMC::initGlobalSettings()
{ {
m_settings.reset(new INISettingsObject("multimc.cfg", this)); m_settings.reset(new INISettingsObject("multimc.cfg", this));

View File

@ -48,6 +48,9 @@ enum UpdateFlag
Q_DECLARE_FLAGS(UpdateFlags, UpdateFlag); Q_DECLARE_FLAGS(UpdateFlags, UpdateFlag);
Q_DECLARE_OPERATORS_FOR_FLAGS(UpdateFlags); 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 class MultiMC : public QApplication
{ {
Q_OBJECT Q_OBJECT

77
WinBacktrace.cpp Normal file
View File

@ -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 <windows.h>
#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;
}

44
WinBacktrace.h Normal file
View File

@ -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 <windows.h>
#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);

View File

@ -709,6 +709,11 @@ void MainWindow::setCatBackground(bool enabled)
void MainWindow::on_actionAddInstance_triggered() void MainWindow::on_actionAddInstance_triggered()
{ {
#ifdef TEST_SEGV
// For further testing stuff.
int v = *((int*)-1);
#endif
if (!MMC->minecraftlist()->isLoaded() && m_versionLoadTask && if (!MMC->minecraftlist()->isLoaded() && m_versionLoadTask &&
m_versionLoadTask->isRunning()) m_versionLoadTask->isRunning())
{ {

View File

@ -1,6 +1,12 @@
#include "MultiMC.h" #include "MultiMC.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
// Crash handling
#ifdef HANDLE_SEGV
#include <HandleCrash.h>
#endif
int main_gui(MultiMC &app) int main_gui(MultiMC &app)
{ {
// show main window // show main window
@ -23,6 +29,11 @@ int main(int argc, char *argv[])
Q_INIT_RESOURCE(multimc); Q_INIT_RESOURCE(multimc);
Q_INIT_RESOURCE(backgrounds); Q_INIT_RESOURCE(backgrounds);
#ifdef HANDLE_SEGV
// Register signal handler for generating crash reports.
initBlackMagic();
#endif
switch (app.status()) switch (app.status())
{ {
case MultiMC::Initialized: case MultiMC::Initialized: