From ed7bf8aa54e080b2cda690abcd5a1a10ded176bf Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:35:53 +0200 Subject: [PATCH 1/6] Add PDB / MAP artifact * update checkout version --- .github/workflows/build.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c1e94b..6595ba2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Get dll version id: dllversion @@ -105,6 +105,16 @@ jobs: # remove --verbose for less verbosity run: cmake --build build --config Release --verbose + - name: Create PDB and MAP artifact + uses: actions/upload-artifact@v7 + with: + name: vSID-PDB_MAP + path: | + build\Release\*.pdb + build\Release\*.map + if-no-files-found: error + retention-days: 14 + - name: Create first zip file (vSID.dll and config.json) shell: pwsh run: | From 4625503f680cf84f53f8c590a3fcce92da42d60b Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:36:41 +0200 Subject: [PATCH 2/6] Add crash handling and debug settings --- CMakeLists.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2db9021..8cf15d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ include_directories(${CMAKE_SOURCE_DIR}/include) set(SOURCES area.cpp configparser.cpp + crashhandler.cpp eseparser.cpp display.cpp flightplan.cpp @@ -101,7 +102,20 @@ set(SOURCES # DLL target add_library(vSID SHARED ${SOURCES}) -target_compile_options(vSID PRIVATE -O2) + +# target_compile_options(vSID PRIVATE -O2) old + +if(MSVC) + # create debug info - /Oy prevents frame pointer omission (for debugging) + target_compile_options(vSID PRIVATE /Zi "$<$:/Oy->") + + # created PDB - /MAP maps function offsets + target_link_options(vSID PRIVATE /DEBUG:FULL /OPT:REF /OPT:ICF "$<$:/MAP>") +endif() + +# set to use MSVC - maybe old +# set_property(TARGET vSID PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + # link curl statically target_compile_definitions(vSID PRIVATE CURL_STATICLIB) @@ -120,4 +134,5 @@ target_link_libraries(vSID PRIVATE ${_curl_target} EuroScopePlugInDLL.lib + dbghelp ) From 35b78c8c51f002b56a7c52a674c95034817c8830 Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:37:09 +0200 Subject: [PATCH 3/6] Update Changelog.txt --- Changelog.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Changelog.txt b/Changelog.txt index b5743cb..58e780e 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,10 @@ +[Changelog v0.15.0] +:gear: **Changed Features / Behaviour** +* GEN - added crash handling + * a .txt file is generated with a small stacktrace showing the line or offset, depending if the pdb file was present at the time of the crash + * a .dmp file is generated (default is a minidump - a fulldump can be generated by adding a file "create_fulldump" without file ending next to the dll) + * the pdb file is generated during the build process + [Changelog v0.14.4] :gear: **Changed Features / Behaviour** * FPLN - startup requests are now blocked if no runway is set (both "normal" startup and rwy startup) From c877acf9711888ec6214a76d184d689498ca556d Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:41:22 +0200 Subject: [PATCH 4/6] Update version --- vSIDPlugin.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vSIDPlugin.h b/vSIDPlugin.h index e9e89f5..5fb1f24 100644 --- a/vSIDPlugin.h +++ b/vSIDPlugin.h @@ -46,7 +46,7 @@ along with this program. If not, see . namespace vsid { const std::string pluginName = "vSID"; - const std::string pluginVersion = "0.14.4"; + const std::string pluginVersion = "0.15.0"; const std::string pluginAuthor = "Gameagle"; const std::string pluginCopyright = "GPL v3"; const std::string pluginViewAviso = ""; From 79b821bd5a31dbda96d91a14e28ec57edd3cb52c Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:43:58 +0200 Subject: [PATCH 5/6] Add date to string function * Change getUtcNow() to inline --- timeHandler.cpp | 5 ----- timeHandler.h | 31 ++++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/timeHandler.cpp b/timeHandler.cpp index 51c4c36..1bb044a 100644 --- a/timeHandler.cpp +++ b/timeHandler.cpp @@ -29,9 +29,4 @@ bool vsid::time::isActive(const std::string& timezone, const int start, const in messageHandler->writeMessage("ERROR", "Timezone failed - " + std::string(e.what()) + ": " + timezone); } return false; -} - -std::chrono::time_point vsid::time::getUtcNow() -{ - return std::chrono::ceil(std::chrono::utc_clock::now()); } \ No newline at end of file diff --git a/timeHandler.h b/timeHandler.h index 0b368e4..c1ca59d 100644 --- a/timeHandler.h +++ b/timeHandler.h @@ -39,11 +39,32 @@ namespace vsid */ bool isActive(const std::string& timezone, const int start, const int end); - /** - * @brief Get the current time in utc (ceiled in seconds) - * - */ - std::chrono::time_point getUtcNow(); + //************************************ + // Description: Get the current time in utc (ceiled in seconds) + // Method: getUtcNow + // FullName: vsid::time::getUtcNow + // Access: public + // Returns: std::chrono::time_point + // Qualifier: + //************************************ + inline std::chrono::time_point getUtcNow() + { + return std::chrono::ceil(std::chrono::utc_clock::now()); + } + + //************************************ + // Description: Get the current date as string in format YYYY-MM-DD + // Method: getDate + // FullName: vsid::time::getDate + // Access: public + // Returns: std::string + // Qualifier: + //************************************ + inline std::string getDate() + { + std::chrono::sys_time sysTime = std::chrono::clock_cast(vsid::time::getUtcNow()); + return std::format("{:%Y-%m-%d}", sysTime); + } /** * @brief Transform a timepoint into a string From 9ed333c8328bba4642ef42c49da3dee2854c0c0c Mon Sep 17 00:00:00 2001 From: Phil7789 <87429599+Phil7789@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:45:11 +0200 Subject: [PATCH 6/6] Add crashhandler --- crashhandler.cpp | 219 +++++++++++++++++++++++++++++++++++++++++++++++ crashhandler.h | 116 +++++++++++++++++++++++++ vSIDPlugin.cpp | 19 +++- 3 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 crashhandler.cpp create mode 100644 crashhandler.h diff --git a/crashhandler.cpp b/crashhandler.cpp new file mode 100644 index 0000000..753729f --- /dev/null +++ b/crashhandler.cpp @@ -0,0 +1,219 @@ +#include "pch.h" + +#include + +#include "crashhandler.h" +#include "messageHandler.h" +#include "timeHandler.h" + +PVOID vehHandler = NULL; + +void vsid::crashhandler::writeStackTrace(EXCEPTION_POINTERS* exceptionInfo) +{ + HMODULE vsidModule = NULL; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR)&EuroScopePlugInInit, + &vsidModule + ); + + std::string crashLogPath = "vsid_crash.txt"; + std::string dumpPath = ""; + char pdbSearchPath[MAX_PATH] = { 0 }; + + if (vsidModule != NULL) + { + char path[MAX_PATH]; + GetModuleFileNameA(vsidModule, path, MAX_PATH); + std::string fullPath(path); + size_t lastSlash = fullPath.find_last_of("\\/"); + if (lastSlash != std::string::npos) + { + crashLogPath = fullPath.substr(0, lastSlash + 1) + "vsid_crash.txt"; + dumpPath = fullPath.substr(0, lastSlash + 1); + strncpy_s(pdbSearchPath, path, lastSlash); + } + } + + vsid::crashhandler::createDumpFile(exceptionInfo, dumpPath); + + std::ofstream logFile(crashLogPath, std::ios::app); + if (!logFile.is_open()) return; + + HANDLE process = GetCurrentProcess(); + + // check for pdb file next to dll + SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS); + SymInitialize(process, (pdbSearchPath[0] == '\0') ? NULL : pdbSearchPath, TRUE); + + DWORD64 crashAdress = (DWORD64)exceptionInfo->ExceptionRecord->ExceptionAddress; + + SYMBOL_INFO* symbol = (SYMBOL_INFO*)calloc(sizeof(SYMBOL_INFO) + 256 * sizeof(char), 1); + symbol->MaxNameLen = 255; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + + IMAGEHLP_LINE64 line; + line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + DWORD displacement; + DWORD64 symDisplacement = 0; + + logFile << "--- CRITICAL ERROR [" << vsid::time::getDate() << " - " << vsid::time::toTimeString(vsid::time::getUtcNow()) << "] ---" << std::endl; + logFile << "Crash Address: 0x" << std::hex << crashAdress << std::dec << std::endl; + + // print the crash address symbol and line info if available otherwise print the offset + if (SymFromAddr(process, crashAdress, &symDisplacement, symbol)) + { + if (SymGetLineFromAddr64(process, crashAdress, &displacement, &line)) + { + const char* shortFilename = strrchr(line.FileName, '\\'); + if (shortFilename == nullptr) shortFilename = strrchr(line.FileName, '/'); + shortFilename = (shortFilename != nullptr) ? shortFilename + 1 : line.FileName; + + logFile << "Crashed at: " << symbol->Name << " in " << shortFilename << " (line: " << line.LineNumber << ")" << std::endl; + } + else + { + logFile << "Crashed at: " << symbol->Name << " (no line info found)" << std::endl; + } + } + else + { + DWORD64 baseAddr = (DWORD64)vsidModule; + DWORD64 offset = crashAdress - baseAddr; + logFile << "Crashed at: [PDB FAILED] Offset 0x" << std::hex << offset << std::dec << " from base of DLL" << std::endl; + logFile << "If no PDB file is available [PDB FAILED] is expected, otherwise this might indicate an issue with the PDB file or symbol loading." << std::endl; + } + + logFile << "Stacktrace:" << std::endl; + + // stack walk + CONTEXT context = *exceptionInfo->ContextRecord; + STACKFRAME64 frame = { 0 }; + + DWORD machineType = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = context.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Esp; + frame.AddrStack.Mode = AddrModeFlat; + + int frameCount = 0; + + // walk the stack to the end or until the set max frames + while (StackWalk64(machineType, process, GetCurrentThread(), &frame, &context, + NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) + { + if (frame.AddrPC.Offset == 0) break; + if (frameCount++ >= vsid::crashhandler::stackFrames) break; + + DWORD64 address = frame.AddrPC.Offset; + + HMODULE frameModule = NULL; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR)address, &frameModule); + + if (frameModule != vsidModule) continue; + + DWORD64 lookupAdress = (address > 0) ? (address - 1) : address; + + if (SymFromAddr(process, lookupAdress, &symDisplacement, symbol)) + { + if (SymGetLineFromAddr64(process, lookupAdress, &displacement, &line)) + { + const char* filename = strrchr(line.FileName, '\\'); + if (filename == nullptr) filename = strrchr(line.FileName, '/'); + filename = (filename != nullptr) ? filename + 1 : line.FileName; + + logFile << "[" << frameCount - 1 << "] " << symbol->Name << " in " << filename << " (line: " << line.LineNumber << ")" << std::endl; + } + else + { + logFile << "[" << frameCount - 1 << "] " << symbol->Name << " (no line info)" << std::endl; + } + } + else + { + DWORD64 offset = lookupAdress - (DWORD64)vsidModule; + logFile << "[" << frameCount - 1 << "] [PDB FAILED] Offset 0x" << std::hex << offset << std::dec << std::endl; + } + } + + logFile << "--- CRITICAL ERROR END [" << vsid::time::getDate() << " - " << vsid::time::toTimeString(vsid::time::getUtcNow()) << "] ---\n" << std::endl; + + free(symbol); + SymCleanup(process); + logFile.close(); +} + +void vsid::crashhandler::initCrashHandler() +{ + if (vehHandler == NULL) vehHandler = AddVectoredExceptionHandler(1, vsid::crashhandler::vSIDCrashHandler); +} + +void vsid::crashhandler::removeCrashHandler() +{ + if (vehHandler != NULL) + { + RemoveVectoredExceptionHandler(vehHandler); + vehHandler = NULL; + } +} + +void vsid::crashhandler::createDumpFile(EXCEPTION_POINTERS* exceptionInfo, const std::string& directoryPath) +{ + // get dump mode for file name + DumpMode dumpMode = getDumpMode(directoryPath); + std::string dumpTypeStr = (dumpMode == vsid::crashhandler::DumpMode::FullDump) ? "FULL" : "MINI"; + + // create dump file name + std::string dumpFilePath = directoryPath + "vsid_dump_" + dumpTypeStr + ".dmp"; + + // create dump file via winapi + HANDLE hFile = CreateFileA( + dumpFilePath.c_str(), + GENERIC_WRITE, + 0, + NULL, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) return; + + // prepare and set exception info + MINIDUMP_EXCEPTION_INFORMATION mdei; + mdei.ThreadId = GetCurrentThreadId(); + mdei.ExceptionPointers = exceptionInfo; + mdei.ClientPointers = FALSE; + + // get dump type + MINIDUMP_TYPE dumpType = MiniDumpNormal; + + // get full dump + if (dumpMode == vsid::crashhandler::DumpMode::FullDump) + dumpType = (MINIDUMP_TYPE)(MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo); + + // write dump + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + dumpType, + (exceptionInfo != NULL) ? &mdei : NULL, + NULL, + NULL + ); + + CloseHandle(hFile); +} + +vsid::crashhandler::DumpMode vsid::crashhandler::getDumpMode(const std::string& directoryPath) +{ + std::string fullDumpTrigger = directoryPath + "create_fulldump"; + + if (GetFileAttributesA(fullDumpTrigger.c_str()) != INVALID_FILE_ATTRIBUTES) + return vsid::crashhandler::DumpMode::FullDump; + else + return vsid::crashhandler::DumpMode::MiniDump; +} \ No newline at end of file diff --git a/crashhandler.h b/crashhandler.h new file mode 100644 index 0000000..9dcdc92 --- /dev/null +++ b/crashhandler.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "include/es/EuroScopePlugIn.h" + +// expose plug-in entry point for crash handler to identify vsid module +void __declspec (dllexport) EuroScopePlugInInit(EuroScopePlugIn::CPlugIn** ppPlugInInstance); + +namespace vsid +{ + namespace crashhandler + { + const int stackFrames = 100; + + enum DumpMode { + None, + MiniDump, + FullDump + }; + + //************************************ + // Description: Walk the stack and write the stack trace to the log file + // Method: writeStackTrace + // FullName: vsid::crashhandler::writeStackTrace + // Access: public + // Returns: void + // Qualifier: + // Parameter: EXCEPTION_POINTERS * exceptionInfo + //************************************ + void writeStackTrace(EXCEPTION_POINTERS* exceptionInfo); + + //************************************ + // Description: Vectored Exception Handler that checks if the crash happened in the vsid module and if so, writes the stack trace to the log file + // Method: vSIDCrashHandler + // FullName: vsid::crashhandler::vSIDCrashHandler + // Access: public + // Returns: LONG WINAPI + // Qualifier: + // Parameter: EXCEPTION_POINTERS * exceptionInfo + //************************************ + inline LONG WINAPI vSIDCrashHandler(EXCEPTION_POINTERS* exceptionInfo) + { + PVOID crashAdress = exceptionInfo->ExceptionRecord->ExceptionAddress; + + // get the crashed module + HMODULE crashedModule = NULL; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR)crashAdress, + &crashedModule + ); + + // get vsid module + HMODULE vsidModule = NULL; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR)&EuroScopePlugInInit, + &vsidModule + ); + + if (crashedModule != NULL && crashedModule == vsidModule) + { + writeStackTrace(exceptionInfo); + } + + return EXCEPTION_CONTINUE_SEARCH; + } + + //************************************ + // Description: Initializes the crash handler + // Method: initCrashHandler + // FullName: vsid::crashhandler::initCrashHandler + // Access: public + // Returns: void + // Qualifier: + //************************************ + void initCrashHandler(); + + //************************************ + // Description: Removes the crash handler + // Method: removeCrashHandler + // FullName: vsid::crashhandler::removeCrashHandler + // Access: public + // Returns: void + // Qualifier: + //************************************ + void removeCrashHandler(); + + //************************************ + // Description: create a dump file. If "create_fulldump" file is present a full dump is generated, otherwise a minidump + // Method: createDumpFile + // FullName: vsid::crashhandler::createDumpFile + // Access: public + // Returns: void + // Qualifier: + // Parameter: EXCEPTION_POINTERS * exceptionInfo + // Parameter: const std::string & directoryPath + //************************************ + void createDumpFile(EXCEPTION_POINTERS* exceptionInfo, const std::string& directoryPath); + + //************************************ + // Description: Checks for the presence of a "create_fulldump" file in the given directory to determine the dump mode + // Method: getDumpMode + // FullName: vsid::crashhandler::getDumpMode + // Access: public + // Returns: vsid::crashhandler::DumpMode + // Qualifier: + // Parameter: const std::string & directoryPath + //************************************ + DumpMode getDumpMode(const std::string& directoryPath); + } +} diff --git a/vSIDPlugin.cpp b/vSIDPlugin.cpp index c81e508..b8ba1e2 100644 --- a/vSIDPlugin.cpp +++ b/vSIDPlugin.cpp @@ -12,6 +12,7 @@ #include "display.h" #include "airport.h" +#include "crashhandler.h" // DEV #include @@ -3461,6 +3462,20 @@ bool vsid::VSIDPlugin::OnCompileCommand(const char* sCommandLine) { std::vector command = vsid::utils::split(sCommandLine, ' '); + /*if (command[0] == ".vsidcrash") // #dev - only for testing crash handling + { + auto triggercrash = []() + { + volatile int* badPointer = nullptr; + + *badPointer = 42; + }; + + triggercrash(); + + return true; + }*/ + if (command[0] == ".vsid") { if (command.size() == 1) @@ -5761,6 +5776,7 @@ void vsid::VSIDPlugin::exit() void __declspec (dllexport) EuroScopePlugInInit(EuroScopePlugIn::CPlugIn** ppPlugInInstance) { + vsid::crashhandler::initCrashHandler(); // create the instance *ppPlugInInstance = vsidPlugin = new vsid::VSIDPlugin(); @@ -5771,7 +5787,8 @@ void __declspec (dllexport) EuroScopePlugInInit(EuroScopePlugIn::CPlugIn** ppPlu void __declspec (dllexport) EuroScopePlugInExit(void) { - /* no deletion of vsidPlugin needed - ownership taken over by this->shared */ + vsid::crashhandler::removeCrashHandler(); + /* no deletion of vsidPlugin needed - ownership taken over by this->shared */ vsidPlugin->exit(); }