Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
* text=auto

*.h linguist-language=C++
*.h.in linguist-language=C++
9 changes: 9 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
cmake -S . -B Build -G "NMake Makefiles" -D CMAKE_BUILD_TYPE=${{ inputs.type }}
cmake --build Build

- name: Test
run: |
& 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1' `
-SkipAutomaticLocation -Arch ${{ inputs.arch == 'x64' && 'amd64' || inputs.arch }}
cmake --build Build --target test

- name: Upload
uses: actions/upload-artifact@v4
with:
Expand All @@ -58,6 +64,9 @@ jobs:
wine cmd /C "C:/SetEnv_${{ inputs.arch }}.bat && cmake -S . -B Build -D CMAKE_BUILD_TYPE=${{ inputs.type }}"
wine cmd /C "C:/SetEnv_${{ inputs.arch }}.bat && cmake --build Build"

- name: Test
run: wine cmd /C "C:/SetEnv_${{ inputs.arch }}.bat && cmake --build Build --target test"

- name: Upload
uses: actions/upload-artifact@v4
with:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Notable changes in each release.
## [Unreleased]
- Fix loading mods via in-game Mods menu ([#69](https://github.com/ccomrade/c1-launcher/pull/69)).
- Revert sys_crashtest improvements ([#71](https://github.com/ccomrade/c1-launcher/pull/71)).
- Crash logger can catch aborts now ([#72](https://github.com/ccomrade/c1-launcher/pull/72)).

## [v7] - 2025-05-11
- Add support for Unicode paths ([#62](https://github.com/ccomrade/c1-launcher/pull/62)).
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,9 @@ set_source_files_properties(Resources/DedicatedServer.rc PROPERTIES OBJECT_DEPEN
set_source_files_properties(Resources/Editor.rc PROPERTIES OBJECT_DEPENDS "${EDITOR_RESOURCES}")

################################################################################

include(CTest)

if(BUILD_TESTING)
add_subdirectory(Tests)
endif()
43 changes: 34 additions & 9 deletions Code/Library/CrashLogger.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <cstdlib>
#include <signal.h>
#include <stdlib.h>

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
Expand All @@ -14,11 +15,12 @@
#define ADDR_FMT "%08X"
#endif

#define CRASH_LOGGER_ABORT 0xE0C1C100
#define CRASH_LOGGER_PURE_CALL 0xE0C1C101
#define CRASH_LOGGER_INVALID_PARAM 0xE0C1C102
#define CRASH_LOGGER_ENGINE_ERROR 0xE0C1C103

static CrashLogger::Handler g_handler;
static CrashLogger::LogFileProvider g_logFileProvider;
static const char* g_banner;

static void* ByteOffset(void* base, std::size_t offset)
Expand Down Expand Up @@ -66,6 +68,7 @@ static const char* ExceptionCodeToName(unsigned int code)
case EXCEPTION_SINGLE_STEP: return "Single step";
case EXCEPTION_STACK_OVERFLOW: return "Stack overflow";
case 0xE06D7363: return "C++ exception";
case CRASH_LOGGER_ABORT: return "Abnormal program termination";
case CRASH_LOGGER_PURE_CALL: return "Pure virtual function call";
case CRASH_LOGGER_INVALID_PARAM: return "Invalid parameter detected by CRT";
case CRASH_LOGGER_ENGINE_ERROR: return "Engine error";
Expand Down Expand Up @@ -409,20 +412,25 @@ static LONG __stdcall CrashHandler(EXCEPTION_POINTERS* exception)
// avoid recursive calls
SetUnhandledExceptionFilter(NULL);

if (g_handler)
if (g_logFileProvider)
{
std::FILE* file = g_handler();
std::FILE* file = g_logFileProvider();
if (file)
{
WriteCrashDump(file, exception);

std::fclose(file);
}
}

return EXCEPTION_CONTINUE_SEARCH;
}

static void AbortHandler(int)
{
RaiseException(CRASH_LOGGER_ABORT, EXCEPTION_NONCONTINUABLE, 0, NULL);
ExitProcess(1);
}

static void PureCallHandler()
{
RaiseException(CRASH_LOGGER_PURE_CALL, EXCEPTION_NONCONTINUABLE, 0, NULL);
Expand All @@ -449,13 +457,16 @@ void CrashLogger::OnEngineError(const char* format, va_list args)
ExitProcess(1);
}

void CrashLogger::Enable(Handler handler, const char* banner)
void CrashLogger::Enable(LogFileProvider logFileProvider, const char* banner)
{
g_handler = handler;
g_logFileProvider = logFileProvider;
g_banner = banner;

SetUnhandledExceptionFilter(&CrashHandler);

signal(SIGABRT, &AbortHandler);
_set_abort_behavior(0, _WRITE_ABORT_MSG); // suppress abort message (console) and dialog (GUI)

_set_purecall_handler(&PureCallHandler);
_set_invalid_parameter_handler(&InvalidParameterHandler);

Expand All @@ -464,17 +475,31 @@ void CrashLogger::Enable(Handler handler, const char* banner)
HMODULE msvcr80 = LoadLibraryA("msvcr80.dll");
if (msvcr80)
{
void* vs2005_signal = GetProcAddress(msvcr80, "signal");
if (vs2005_signal)
{
reinterpret_cast<void(*(*)(int, void(*)(int)))(int)>
(vs2005_signal)(SIGABRT, &AbortHandler);
}

void* vs2005_set_abort_behavior = GetProcAddress(msvcr80, "_set_abort_behavior");
if (vs2005_set_abort_behavior)
{
reinterpret_cast<unsigned int(*)(unsigned int, unsigned int)>
(vs2005_set_abort_behavior)(0, _WRITE_ABORT_MSG);
}

void* vs2005_set_purecall_handler = GetProcAddress(msvcr80, "_set_purecall_handler");
if (vs2005_set_purecall_handler)
{
static_cast<_purecall_handler(*)(_purecall_handler)>
reinterpret_cast<_purecall_handler(*)(_purecall_handler)>
(vs2005_set_purecall_handler)(&PureCallHandler);
}

void* vs2005_set_invalid_parameter_handler = GetProcAddress(msvcr80, "_set_invalid_parameter_handler");
if (vs2005_set_invalid_parameter_handler)
{
static_cast<_invalid_parameter_handler(*)(_invalid_parameter_handler)>
reinterpret_cast<_invalid_parameter_handler(*)(_invalid_parameter_handler)>
(vs2005_set_invalid_parameter_handler)(&InvalidParameterHandler);
}
}
Expand Down
4 changes: 2 additions & 2 deletions Code/Library/CrashLogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace CrashLogger
{
typedef std::FILE* (*Handler)();
typedef std::FILE* (*LogFileProvider)();

void OnEngineError(const char* format, va_list args);

void Enable(Handler handler, const char* banner);
void Enable(LogFileProvider logFileProvider, const char* banner);
}
13 changes: 13 additions & 0 deletions Tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
add_executable(CrashLoggerTests CrashLoggerTests.cpp)
target_link_libraries(CrashLoggerTests PUBLIC LauncherBase)

add_test(NAME CrashLoggerTests_SegFault COMMAND $<TARGET_FILE:CrashLoggerTests> SegFault)
add_test(NAME CrashLoggerTests_IntDivisionByZero COMMAND $<TARGET_FILE:CrashLoggerTests> IntDivisionByZero)
add_test(NAME CrashLoggerTests_InvalidCrtArgument COMMAND $<TARGET_FILE:CrashLoggerTests> InvalidCrtArgument)
add_test(NAME CrashLoggerTests_PureVirtualCall COMMAND $<TARGET_FILE:CrashLoggerTests> PureVirtualCall)
add_test(NAME CrashLoggerTests_EngineError COMMAND $<TARGET_FILE:CrashLoggerTests> EngineError)
add_test(NAME CrashLoggerTests_DebugBreak COMMAND $<TARGET_FILE:CrashLoggerTests> DebugBreak)
add_test(NAME CrashLoggerTests_Halt COMMAND $<TARGET_FILE:CrashLoggerTests> Halt)
add_test(NAME CrashLoggerTests_UnhandledCppException COMMAND $<TARGET_FILE:CrashLoggerTests> UnhandledCppException)
add_test(NAME CrashLoggerTests_StdAbort COMMAND $<TARGET_FILE:CrashLoggerTests> StdAbort)
add_test(NAME CrashLoggerTests_StdTerminate COMMAND $<TARGET_FILE:CrashLoggerTests> StdTerminate)
140 changes: 140 additions & 0 deletions Tests/CrashLoggerTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <intrin.h>

#include "Library/CrashLogger.h"

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))

static void Test_SegFault()
{
volatile int* p = NULL;
*p = 0xabcd;
}

static void Test_IntDivisionByZero()
{
volatile int i = 0;
i /= i;
}

static void Test_InvalidCrtArgument()
{
strcpy_s(NULL, 4, "abc");
}

static void Test_PureVirtualCall()
{
struct Base
{
Base()
{
this->Boom();
}

void Boom()
{
this->Call();
}

virtual void Call() = 0;
};

struct Derived : Base
{
void Call() override
{
}
};

Derived d;
}

static void CallEngineError(const char* format, ...)
{
va_list args;
va_start(args, format);
CrashLogger::OnEngineError(format, args);
va_end(args);
}

static void Test_EngineError()
{
CallEngineError("engine error test");
}

static void Test_DebugBreak()
{
__debugbreak();
}

static void Test_Halt()
{
__halt();
}

static void Test_UnhandledCppException()
{
throw 666;
}

static void Test_StdAbort()
{
std::abort();
}

static void Test_StdTerminate()
{
std::terminate();
}

static const struct { const char* name; void (*func)(); } TESTS[] = {
{ "SegFault", &Test_SegFault },
{ "IntDivisionByZero", &Test_IntDivisionByZero },
{ "InvalidCrtArgument", &Test_InvalidCrtArgument },
{ "PureVirtualCall", &Test_PureVirtualCall },
{ "EngineError", &Test_EngineError },
{ "DebugBreak", &Test_DebugBreak },
{ "Halt", &Test_Halt },
{ "UnhandledCppException", &Test_UnhandledCppException },
{ "StdAbort", &Test_StdAbort },
{ "StdTerminate", &Test_StdTerminate },
};

static std::FILE* MockLogFileProvider()
{
// crash successfully caught
std::exit(EXIT_SUCCESS);
}

int main(int argc, char** argv)
{
if (argc != 2)
{
std::fprintf(stderr, "Usage: %s TEST\n", argv[0]);
return EXIT_FAILURE;
}

const char* test = argv[1];

// initialize crash logger
CrashLogger::Enable(&MockLogFileProvider, "Test banner");

for (std::size_t i = 0; i < ARRAY_SIZE(TESTS); i++)
{
if (std::strcmp(TESTS[i].name, test) == 0)
{
// execute the test
TESTS[i].func();

// all tests should trigger a crash and never return
return EXIT_FAILURE;
}
}

// test not found
return EXIT_FAILURE;
}