diff --git a/.gitattributes b/.gitattributes index 1427df7..b5768f9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,2 @@ -* text=auto - *.h linguist-language=C++ *.h.in linguist-language=C++ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29dfebd..b74563c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index e7438f7..dd27aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/CMakeLists.txt b/CMakeLists.txt index 7577ea8..12ce80c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/Code/Library/CrashLogger.cpp b/Code/Library/CrashLogger.cpp index 26d5070..f41c191 100644 --- a/Code/Library/CrashLogger.cpp +++ b/Code/Library/CrashLogger.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #define WIN32_LEAN_AND_MEAN #include @@ -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) @@ -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"; @@ -409,13 +412,12 @@ 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); } } @@ -423,6 +425,12 @@ static LONG __stdcall CrashHandler(EXCEPTION_POINTERS* exception) 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); @@ -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); @@ -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 + (vs2005_signal)(SIGABRT, &AbortHandler); + } + + void* vs2005_set_abort_behavior = GetProcAddress(msvcr80, "_set_abort_behavior"); + if (vs2005_set_abort_behavior) + { + reinterpret_cast + (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); } } diff --git a/Code/Library/CrashLogger.h b/Code/Library/CrashLogger.h index 0e5c536..e260870 100644 --- a/Code/Library/CrashLogger.h +++ b/Code/Library/CrashLogger.h @@ -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); } diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..1eec07c --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,13 @@ +add_executable(CrashLoggerTests CrashLoggerTests.cpp) +target_link_libraries(CrashLoggerTests PUBLIC LauncherBase) + +add_test(NAME CrashLoggerTests_SegFault COMMAND $ SegFault) +add_test(NAME CrashLoggerTests_IntDivisionByZero COMMAND $ IntDivisionByZero) +add_test(NAME CrashLoggerTests_InvalidCrtArgument COMMAND $ InvalidCrtArgument) +add_test(NAME CrashLoggerTests_PureVirtualCall COMMAND $ PureVirtualCall) +add_test(NAME CrashLoggerTests_EngineError COMMAND $ EngineError) +add_test(NAME CrashLoggerTests_DebugBreak COMMAND $ DebugBreak) +add_test(NAME CrashLoggerTests_Halt COMMAND $ Halt) +add_test(NAME CrashLoggerTests_UnhandledCppException COMMAND $ UnhandledCppException) +add_test(NAME CrashLoggerTests_StdAbort COMMAND $ StdAbort) +add_test(NAME CrashLoggerTests_StdTerminate COMMAND $ StdTerminate) diff --git a/Tests/CrashLoggerTests.cpp b/Tests/CrashLoggerTests.cpp new file mode 100644 index 0000000..babdbe6 --- /dev/null +++ b/Tests/CrashLoggerTests.cpp @@ -0,0 +1,140 @@ +#include +#include +#include +#include +#include +#include + +#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; +}