diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 758dbc527e..e620718c1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,11 +19,11 @@ jobs: fail-fast: false matrix: platform: - - { name: Windows (mingw32), os: windows-latest, shell: 'msys2 {0}', msystem: mingw32, msys-env: mingw-w64-i686, artifact: 'SDL-mingw32', no-perl: true } - - { name: Windows (mingw64), os: windows-latest, shell: 'msys2 {0}', msystem: mingw64, msys-env: mingw-w64-x86_64, artifact: 'SDL-mingw64' } - - { name: Windows (clang32), os: windows-latest, shell: 'msys2 {0}', msystem: clang32, msys-env: mingw-w64-clang-i686, artifact: 'SDL-msys2-clang32', no-perl: true } - - { name: Windows (clang64), os: windows-latest, shell: 'msys2 {0}', msystem: clang64, msys-env: mingw-w64-clang-x86_64, artifact: 'SDL-msys2-clang64' } - - { name: Windows (ucrt64), os: windows-latest, shell: 'msys2 {0}', msystem: ucrt64, msys-env: mingw-w64-ucrt-x86_64, artifact: 'SDL-msys2-ucrt64' } + - { name: Windows (mingw32), os: windows-latest, shell: 'msys2 {0}', msystem: mingw32, msys-env: mingw-w64-i686, cmake: '-DSDLTEST_PROCDUMP=ON', artifact: 'SDL-mingw32', no-perl: true } + - { name: Windows (mingw64), os: windows-latest, shell: 'msys2 {0}', msystem: mingw64, msys-env: mingw-w64-x86_64, cmake: '-DSDLTEST_PROCDUMP=ON', artifact: 'SDL-mingw64' } + - { name: Windows (clang32), os: windows-latest, shell: 'msys2 {0}', msystem: clang32, msys-env: mingw-w64-clang-i686, cmake: '-DSDLTEST_PROCDUMP=ON', artifact: 'SDL-msys2-clang32', no-perl: true } + - { name: Windows (clang64), os: windows-latest, shell: 'msys2 {0}', msystem: clang64, msys-env: mingw-w64-clang-x86_64, cmake: '-DSDLTEST_PROCDUMP=ON', artifact: 'SDL-msys2-clang64' } + - { name: Windows (ucrt64), os: windows-latest, shell: 'msys2 {0}', msystem: ucrt64, msys-env: mingw-w64-ucrt-x86_64, cmake: '-DSDLTEST_PROCDUMP=ON', artifact: 'SDL-msys2-ucrt64' } - { name: Ubuntu 20.04, os: ubuntu-20.04, shell: sh, artifact: 'SDL-ubuntu20.04' } - { name: Intel oneAPI (Ubuntu 20.04), os: ubuntu-20.04, shell: bash, artifact: 'SDL-ubuntu20.04-oneapi', intel: true, source_cmd: 'source /opt/intel/oneapi/setvars.sh; export CC=icx; export CXX=icx;'} @@ -121,6 +121,7 @@ jobs: ${{ matrix.platform.source_cmd }} cmake --build build/ --config Release --verbose --parallel - name: Run build-time tests (CMake) + id: tests if: ${{ !matrix.platform.cross-build }} run: | ${{ matrix.platform.source_cmd }} @@ -172,6 +173,12 @@ jobs: ${{ matrix.platform.source_cmd }} export PKG_CONFIG_PATH=$(echo "${{ github.workspace }}/cmake_prefix/lib/pkgconfig" | sed -e 's#\\#/#g') cmake/test/test_pkgconfig.sh + - uses: actions/upload-artifact@v4 + if: ${{ always() && steps.tests.outcome == 'failure' }} + with: + if-no-files-found: ignore + name: '${{ matrix.platform.artifact }}-minidumps' + path: build/minidumps/* - uses: actions/upload-artifact@v4 if: ${{ always() && matrix.platform.artifact != '' && steps.build.outcome == 'success' }} with: diff --git a/.github/workflows/msvc.yml b/.github/workflows/msvc.yml index fa2b405f23..6cde63bff6 100644 --- a/.github/workflows/msvc.yml +++ b/.github/workflows/msvc.yml @@ -60,6 +60,7 @@ jobs: -DSDL_DISABLE_INSTALL=OFF ` -DSDL_DISABLE_INSTALL_CPACK=OFF ` -DSDL_DISABLE_INSTALL_DOCS=OFF ` + -DSDLTEST_PROCDUMP=ON ` ${{ matrix.platform.flags }} ` -DCMAKE_INSTALL_PREFIX=prefix - name: Build (CMake) @@ -67,6 +68,7 @@ jobs: run: | cmake --build build/ --config Release --parallel - name: Run build-time tests + id: tests if: ${{ !matrix.platform.notests }} run: | $env:SDL_TESTS_QUICK=1 @@ -85,13 +87,18 @@ jobs: -DCMAKE_PREFIX_PATH=${{ env.SDL3_DIR }} ` ${{ matrix.platform.flags }} cmake --build cmake_config_build --config Release - - name: Add msbuild to PATH if: ${{ matrix.platform.project != '' }} uses: microsoft/setup-msbuild@v2 - name: Build msbuild if: ${{ matrix.platform.project != '' }} run: msbuild ${{ matrix.platform.project }} /m /p:BuildInParallel=true /p:Configuration=Release ${{ matrix.platform.projectflags }} + - uses: actions/upload-artifact@v4 + if: ${{ always() && steps.tests.outcome == 'failure' }} + with: + if-no-files-found: ignore + name: '${{ matrix.platform.artifact }}-minidumps' + path: build/minidumps/* - uses: actions/upload-artifact@v4 if: ${{ always() && steps.build.outcome == 'success' }} with: diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d4fce68c8c..32e952d19e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -38,6 +38,17 @@ target_link_libraries(sdltests_utils PRIVATE SDL3::Headers) file(GLOB RESOURCE_FILES *.bmp *.wav *.hex moose.dat utf8.txt) +if(WIN32 AND NOT WINDOWS_STORE) + option(SDLTEST_PROCDUMP "Run tests using sdlprocdump for minidump generation" OFF) + add_executable(sdlprocdump win32/sdlprocdump.c) + SDL_AddCommonCompilerFlags(sdlprocdump) + if(SDLTEST_PROCDUMP) + set(CMAKE_TEST_LAUNCHER "$") + else() + set_property(TARGET sdlprocdump PROPERTY EXCLUDE_FROM_ALL "1") + endif() +endif() + if(CMAKE_RUNTIME_OUTPUT_DIRECTORY) set(test_bin_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") if(NOT IS_ABSOLUTE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") diff --git a/test/win32/sdlprocdump.c b/test/win32/sdlprocdump.c new file mode 100644 index 0000000000..c5fab0994f --- /dev/null +++ b/test/win32/sdlprocdump.c @@ -0,0 +1,269 @@ +#include +#include + +#include +#include + +#define APPNAME "[SDLPROCDUMP]" +#define DUMP_FOLDER "minidumps" + +typedef BOOL (WINAPI *MiniDumpWriteDumpFuncType)( + HANDLE hProcess, + DWORD ProcessId, + HANDLE hFile, + MINIDUMP_TYPE DumpType, + PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, + PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, + PMINIDUMP_CALLBACK_INFORMATION CallbackParam); + +#define FOREACH_EXCEPTION_CODES(X) \ + X(EXCEPTION_ACCESS_VIOLATION) \ + X(EXCEPTION_DATATYPE_MISALIGNMENT) \ + X(EXCEPTION_BREAKPOINT) \ + X(EXCEPTION_SINGLE_STEP) \ + X(EXCEPTION_ARRAY_BOUNDS_EXCEEDED) \ + X(EXCEPTION_FLT_DENORMAL_OPERAND) \ + X(EXCEPTION_FLT_DIVIDE_BY_ZERO) \ + X(EXCEPTION_FLT_INEXACT_RESULT) \ + X(EXCEPTION_FLT_INVALID_OPERATION) \ + X(EXCEPTION_FLT_OVERFLOW) \ + X(EXCEPTION_FLT_STACK_CHECK) \ + X(EXCEPTION_FLT_UNDERFLOW) \ + X(EXCEPTION_INT_DIVIDE_BY_ZERO) \ + X(EXCEPTION_INT_OVERFLOW) \ + X(EXCEPTION_PRIV_INSTRUCTION) \ + X(EXCEPTION_IN_PAGE_ERROR) \ + X(EXCEPTION_ILLEGAL_INSTRUCTION) \ + X(EXCEPTION_NONCONTINUABLE_EXCEPTION) \ + X(EXCEPTION_STACK_OVERFLOW) \ + X(EXCEPTION_INVALID_DISPOSITION) \ + X(EXCEPTION_GUARD_PAGE) \ + X(EXCEPTION_INVALID_HANDLE) \ + X(STATUS_HEAP_CORRUPTION) + +static const char *exceptionCode_to_string(DWORD dwCode) { +#define SWITCH_CODE_STR(V) case V: return #V; + switch (dwCode) { + FOREACH_EXCEPTION_CODES(SWITCH_CODE_STR) + default: { + static const char unknown[] = "unknown"; + return unknown; + } + } +#undef SWITCH_CODE_STR +} + +static int IsFatalExceptionCode(DWORD dwCode) { + switch (dwCode) { + case EXCEPTION_ACCESS_VIOLATION: + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + case EXCEPTION_IN_PAGE_ERROR: + case EXCEPTION_INT_DIVIDE_BY_ZERO: + case EXCEPTION_STACK_OVERFLOW: + case STATUS_HEAP_CORRUPTION: + case STATUS_STACK_BUFFER_OVERRUN: + case EXCEPTION_GUARD_PAGE: + case EXCEPTION_INVALID_HANDLE: + return 1; + default: + return 0; + } +} + +static void format_windows_error_message(const char *message) { + char win_msg[512]; + FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + win_msg, sizeof(win_msg)/sizeof(*win_msg), + NULL); + size_t win_msg_len = strlen(win_msg); + while (win_msg[win_msg_len-1] == '\r' || win_msg[win_msg_len-1] == '\n' || win_msg[win_msg_len-1] == ' ') { + win_msg[win_msg_len-1] = '\0'; + win_msg_len--; + } + fprintf(stderr, "%s %s (%s)\n", APPNAME, message, win_msg); +} + +static void create_minidump(const char *child_file_path, const LPPROCESS_INFORMATION process_information, DWORD dwThreadId) { + BOOL success; + char dump_file_path[MAX_PATH]; + char child_file_name[64]; + HANDLE hFile = INVALID_HANDLE_VALUE; + HMODULE dbghelp_module = NULL; + MiniDumpWriteDumpFuncType MiniDumpWriteDumpFunc = NULL; + MINIDUMP_EXCEPTION_INFORMATION minidump_exception_information; + SYSTEMTIME system_time; + + success = CreateDirectoryA(DUMP_FOLDER, NULL); + if (!success && GetLastError() != ERROR_ALREADY_EXISTS) { + format_windows_error_message("Failed to create minidump directory"); + goto post_dump; + } + _splitpath_s(child_file_path, NULL, 0, NULL, 0, child_file_name, sizeof(child_file_name), NULL, 0); + GetLocalTime(&system_time); + + snprintf(dump_file_path, sizeof(dump_file_path), "minidumps/%s_%04d-%02d-%02d_%d-%02d-%02d.dmp", + child_file_name, + system_time.wYear, system_time.wMonth, system_time.wDay, + system_time.wHour, system_time.wMinute, system_time.wSecond); + fprintf(stderr, "%s Writing minidump to \"%s\"\n", APPNAME, dump_file_path); + hFile = CreateFileA( + dump_file_path, + GENERIC_WRITE, + FILE_SHARE_WRITE, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL); + if (hFile == INVALID_HANDLE_VALUE) { + format_windows_error_message("Failed to open file for minidump"); + goto post_dump; + } + dbghelp_module = LoadLibraryA("dbghelp.dll"); + if (!dbghelp_module) { + format_windows_error_message("Failed to load dbghelp.dll"); + goto post_dump; + } + MiniDumpWriteDumpFunc = (MiniDumpWriteDumpFuncType)GetProcAddress(dbghelp_module, "MiniDumpWriteDump"); + if (!MiniDumpWriteDumpFunc) { + format_windows_error_message("Failed to find MiniDumpWriteDump in dbghelp.dll"); + goto post_dump; + } + minidump_exception_information.ClientPointers = FALSE; + minidump_exception_information.ExceptionPointers = FALSE; + minidump_exception_information.ThreadId = dwThreadId; + success = MiniDumpWriteDumpFunc( + process_information->hProcess, /* HANDLE hProcess */ + process_information->dwProcessId, /* DWORD ProcessId */ + hFile, /* HANDLE hFile */ + MiniDumpWithFullMemory, /* MINIDUMP_TYPE DumpType */ + &minidump_exception_information, /* PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam */ + NULL, /* PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam */ + NULL); /* PMINIDUMP_CALLBACK_INFORMATION CallbackParam */ + if (!success) { + format_windows_error_message("Failed to write minidump"); + } +post_dump: + if (hFile != INVALID_HANDLE_VALUE) { + CloseHandle(hFile); + } + if (dbghelp_module != NULL) { + FreeLibrary(dbghelp_module); + } +} + +int main(int argc, char *argv[]) { + int i; + size_t command_line_len = 0; + char *command_line; + STARTUPINFOA startup_info; + PROCESS_INFORMATION process_information; + BOOL success; + BOOL debugger_present; + DWORD exit_code; + DWORD creation_flags; + + if (argc < 2) { + fprintf(stderr, "Usage: %s PROGRAM [ARG1 [ARG2 [ARG3 ... ]]]\n", argv[0]); + return 1; + } + + for (i = 1; i < argc; i++) { + command_line_len += strlen(argv[1]) + 1; + } + command_line = malloc(command_line_len + 1); + if (!command_line) { + fprintf(stderr, "%s Failed to allocate memory for command line\n", APPNAME); + return 1; + } + command_line[0] = '\0'; + for (i = 1; i < argc; i++) { + strcat_s(command_line, command_line_len, argv[i]); + if (i != argc - 1) { + strcat_s(command_line, command_line_len, " "); + } + } + + memset(&startup_info, 0, sizeof(startup_info)); + startup_info.cb = sizeof(startup_info); + + debugger_present = IsDebuggerPresent(); + creation_flags = NORMAL_PRIORITY_CLASS; + if (!debugger_present) { + creation_flags |= DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS; + } + success = CreateProcessA( + argv[1], /* LPCSTR lpApplicationName, */ + command_line, /* LPSTR lpCommandLine, */ + NULL, /* LPSECURITY_ATTRIBUTES lpProcessAttributes, */ + NULL, /* LPSECURITY_ATTRIBUTES lpThreadAttributes, */ + TRUE, /* BOOL bInheritHandles, */ + creation_flags, /* DWORD dwCreationFlags, */ + NULL, /* LPVOID lpEnvironment, */ + NULL, /* LPCSTR lpCurrentDirectory, */ + &startup_info, /* LPSTARTUPINFOA lpStartupInfo, */ + &process_information); /* LPPROCESS_INFORMATION lpProcessInformation */ + + if (!success) { + fprintf(stderr, "%s Failed to start application\n", APPNAME); + return 1; + } + + if (debugger_present) { + WaitForSingleObject(process_information.hProcess, INFINITE); + } else { + int process_alive = 1; + DEBUG_EVENT event; + while (process_alive) { + DWORD continue_status = DBG_CONTINUE; + success = WaitForDebugEvent(&event, INFINITE); + if (!success) { + fprintf(stderr, "%s Failed to get a debug event\n", APPNAME); + return 1; + } + switch (event.dwDebugEventCode) { + case EXCEPTION_DEBUG_EVENT: + if (IsFatalExceptionCode(event.u.Exception.ExceptionRecord.ExceptionCode) || (event.u.Exception.ExceptionRecord.ExceptionFlags & EXCEPTION_NONCONTINUABLE)) { + fprintf(stderr, "%s EXCEPTION_DEBUG_EVENT ExceptionCode: 0x%08lx (%s) ExceptionFlags: 0x%08lx\n", + APPNAME, + event.u.Exception.ExceptionRecord.ExceptionCode, + exceptionCode_to_string(event.u.Exception.ExceptionRecord.ExceptionCode), + event.u.Exception.ExceptionRecord.ExceptionFlags); + fprintf(stderr, "%s Non-continuable exception debug event\n", APPNAME); + create_minidump(argv[1], &process_information, event.dwThreadId); + DebugActiveProcessStop(event.dwProcessId); + process_alive = 0; + } + continue_status = DBG_EXCEPTION_HANDLED; + break; + case EXIT_PROCESS_DEBUG_EVENT: + exit_code = event.u.ExitProcess.dwExitCode; + if (event.dwProcessId == process_information.dwProcessId) { + process_alive = 0; + DebugActiveProcessStop(event.dwProcessId); + } + break; + } + success = ContinueDebugEvent(event.dwProcessId, event.dwThreadId, continue_status); + if (!process_alive) { + DebugActiveProcessStop(event.dwProcessId); + } + } + } + + exit_code = 1; + success = GetExitCodeProcess(process_information.hProcess, &exit_code); + + if (!success) { + fprintf(stderr, "%s Failed to get process exit code\n", APPNAME); + return 1; + } + + CloseHandle(process_information.hThread); + CloseHandle(process_information.hProcess); + + return exit_code; +}