VEX Engine
Retro looking game engine made in vulkan mostly because i wanted to understand it better.
Loading...
Searching...
No Matches
ErrorUtils.cpp
1#include "components/ErrorUtils.hpp"
2#include "components/PathUtils.hpp"
3#include "components/HardwareInfo.hpp"
4
5#include <iostream>
6#include <sstream>
7#include <iomanip>
8#include <chrono>
9#include <ctime>
10#include <SDL3/SDL_log.h>
11#include <vector>
12#include <array>
13#include <mutex>
14#include <cstring>
15#include <atomic>
16#include <algorithm>
17
18#include <fcntl.h>
19#include <sys/types.h>
20#include <sys/stat.h>
21#include <thread>
22
23#if defined(_WIN32)
24 #include <shellapi.h>
25 #include <windows.h>
26 #include <psapi.h>
27 #include <io.h>
28 #define WRITE_FUNC _write
29 #define SYNC_FUNC _commit
30 #define OPEN_FUNC _open
31 #define CLOSE_FUNC _close
32 #define READ_FUNC _read
33 #define O_FLAGS _O_CREAT | _O_TRUNC | _O_WRONLY
34 #define O_RDONLY _O_RDONLY
35 #define S_FLAGS _S_IWRITE
36#else
37 #include <signal.h>
38 #include <ucontext.h>
39 #include <unistd.h>
40 #define WRITE_FUNC write
41 #define SYNC_FUNC fsync
42 #define OPEN_FUNC open
43 #define CLOSE_FUNC close
44 #define READ_FUNC read
45 #define O_FLAGS O_CREAT | O_TRUNC | O_WRONLY
46 #define S_FLAGS S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
47#endif
48
49#if DEBUG
50#include <cpptrace/cpptrace.hpp>
51#endif
52
53namespace vex {
54 static std::vector<LogCallbackFn> gLogCallbacks;
55 static std::mutex gCallbackMutex;
56
58 std::lock_guard<std::mutex> lock(gCallbackMutex);
59 gLogCallbacks.push_back(callback);
60 }
61
62 static void showCrashDialog(const std::string& logPath) {
63 #if defined(_WIN32)
64 char tempPath[MAX_PATH];
65 GetTempPathA(MAX_PATH, tempPath);
66 std::string psScriptPath = std::string(tempPath) + "vex_crash_reporter.ps1";
67
68 std::string psCode =
69 "Add-Type -AssemblyName System.Windows.Forms\n"
70 "Add-Type -AssemblyName System.Drawing\n"
71 "$form = New-Object System.Windows.Forms.Form\n"
72 "$form.Text = 'Vex Engine Crash Reporter'\n"
73 "$form.Size = New-Object System.Drawing.Size(800, 600)\n"
74 "$form.StartPosition = 'CenterScreen'\n"
75 "\n"
76 "$label = New-Object System.Windows.Forms.Label\n"
77 "$label.Text = 'Sorry, the application has crashed. Please share this log with the developer.'\n"
78 "$label.AutoSize = $true\n"
79 "$label.Location = New-Object System.Drawing.Point(10, 10)\n"
80 "$form.Controls.Add($label)\n"
81 "\n"
82 "$textBox = New-Object System.Windows.Forms.TextBox\n"
83 "$textBox.Multiline = $true\n"
84 "$textBox.ScrollBars = 'Vertical'\n"
85 "$textBox.ReadOnly = $true\n"
86 "$textBox.Location = New-Object System.Drawing.Point(10, 40)\n"
87 "$textBox.Size = New-Object System.Drawing.Size(760, 500)\n"
88 "$textBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right\n"
89 "$textBox.Font = New-Object System.Drawing.Font('Consolas', 10)\n"
90 "\n"
91 "if (Test-Path '" + logPath + "') {\n"
92 " $textBox.Text = Get-Content '" + logPath + "' -Raw\n"
93 "} else {\n"
94 " $textBox.Text = 'Error: Log file not found at " + logPath + "'\n"
95 "}\n"
96 "\n"
97 "$form.Controls.Add($textBox)\n"
98 "$form.ShowDialog()";
99
100 FILE* fp = fopen(psScriptPath.c_str(), "w");
101 if (fp) {
102 fprintf(fp, "%s", psCode.c_str());
103 fclose(fp);
104
105 ShellExecuteA(NULL, "open", "powershell.exe",
106 ("-ExecutionPolicy Bypass -WindowStyle Hidden -File \"" + psScriptPath + "\"").c_str(),
107 NULL, SW_HIDE);
108 } else {
109 ShellExecuteA(NULL, "open", "notepad.exe", logPath.c_str(), NULL, SW_SHOWNORMAL);
110 }
111
112 #elif defined(__linux__)
113 pid_t pid = fork();
114 if (pid == 0) {
115 setsid();
116
117 execlp("zenity", "zenity",
118 "--text-info",
119 "--filename", logPath.c_str(),
120 "--title", "Vex Engine Crash Reporter",
121 "--width=800", "--height=600",
122 "--font=Monospace 10",
123 (char*)NULL);
124
125 execlp("kdialog", "kdialog",
126 "--title", "Vex Engine Crash Reporter",
127 "--textbox", logPath.c_str(),
128 "800", "600",
129 (char*)NULL);
130
131 execlp("xterm", "xterm", "-hold", "-e", "cat", logPath.c_str(), (char*)NULL);
132
133 _exit(1);
134 }
135 #endif
136 }
137
138 struct LogEntry {
139 char m_time[16];
140 char m_level[8];
141 char m_msg[256];
142 };
143
144 static std::array<LogEntry, 200> gRingBuffer;
145 static size_t gRingHead = 0;
146 static std::mutex gRingMutex;
147
148 static std::atomic<bool> gIsCrashing(false);
149 static int gCrashLogFd = -1;
150
151 static std::string getTimeStr() {
152 auto now = std::chrono::system_clock::now();
153 auto inTimeT = std::chrono::system_clock::to_time_t(now);
154 std::stringstream ss;
155 ss << std::put_time(std::localtime(&inTimeT), "%Y-%m-%d %X");
156 return ss.str();
157 }
158
159 static const char* getLevelStr(LogLevel level) {
160 switch (level) {
161 case LogLevel::INFO: return "INFO";
162 case LogLevel::WARNING: return "WARNING";
163 case LogLevel::ERROR: return "ERROR";
164 case LogLevel::CRITICAL: return "CRITICAL";
165 default: return "UNKNOWN";
166 }
167 }
168
169 static void pushToRing(LogLevel level, const char* msg) {
170 if(gIsCrashing) return;
171
172 std::unique_lock<std::mutex> lock(gRingMutex, std::defer_lock);
173 if(!lock.try_lock()) return;
174
175 LogEntry& entry = gRingBuffer[gRingHead];
176
177 time_t rawtime;
178 time(&rawtime);
179 struct tm* timeinfo = localtime(&rawtime);
180 strftime(entry.m_time, sizeof(entry.m_time), "%H:%M:%S", timeinfo);
181
182 const char* lvl = getLevelStr(level);
183 strncpy(entry.m_level, lvl, 7); entry.m_level[7] = '\0';
184 strncpy(entry.m_msg, msg, 255); entry.m_msg[255] = '\0';
185
186 gRingHead = (gRingHead + 1) % gRingBuffer.size();
187 }
188
189 static void safeWriteStr(int fd, const char* s) {
190 if (!s || fd < 0) return;
191
192 for (size_t i = 0; s[i] != '\0'; ++i) {
193 unsigned char c = (unsigned char)s[i];
194 if (c == 9 || c == 10 || c == 13 || (c >= 32 && c <= 126)) {
195 WRITE_FUNC(fd, &c, 1);
196 } else {
197 const char replacement = '?';
198 WRITE_FUNC(fd, &replacement, 1);
199 }
200 }
201 }
202
203 static void safeWriteHex(int fd, uintptr_t val) {
204 if (fd < 0) return;
205 char buf[32];
206 int i = 30;
207 buf[31] = '\n';
208
209 if (val == 0) {
210 safeWriteStr(fd, "0x0\n");
211 return;
212 }
213
214 while (val > 0 && i > 1) {
215 uintptr_t digit = val % 16;
216 buf[i] = (digit < 10) ? ('0' + digit) : ('a' + (digit - 10));
217 val /= 16;
218 i--;
219 }
220 buf[i] = 'x';
221 buf[i-1] = '0';
222 WRITE_FUNC(fd, &buf[i-1], 32 - (i-1));
223 }
224
225 static void dumpProcessMap(int outFd) {
226 safeWriteStr(outFd, "\n--- MEMORY MAP (Base Addresses) ---\n");
227 #ifdef __linux__
228 int mapFd = OPEN_FUNC("/proc/self/maps", O_RDONLY, 0);
229 if (mapFd >= 0) {
230 char buf[2048];
231 ssize_t bytesRead;
232 while ((bytesRead = READ_FUNC(mapFd, buf, sizeof(buf))) > 0) {
233 WRITE_FUNC(outFd, buf, bytesRead);
234 }
235 CLOSE_FUNC(mapFd);
236 }
237 #elif defined(_WIN32)
238 HANDLE hProcess = GetCurrentProcess();
239 HMODULE hMods[1024];
240 DWORD cbNeeded;
241 if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
242 for (unsigned int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
243 char szModName[MAX_PATH];
244 if (GetModuleFileNameA(hMods[i], szModName, sizeof(szModName))) {
245 MODULEINFO modInfo;
246 if(GetModuleInformation(hProcess, hMods[i], &modInfo, sizeof(modInfo))) {
247 safeWriteStr(outFd, "Base: ");
248 safeWriteHex(outFd, (uintptr_t)modInfo.lpBaseOfDll);
249 safeWriteStr(outFd, " | ");
250 safeWriteStr(outFd, szModName);
251 safeWriteStr(outFd, "\n");
252 }
253 }
254 }
255 }
256 #endif
257 safeWriteStr(outFd, "-----------------------------------\n");
258 }
259
260 static void dumpRegisters(int fd, void* context) {
261 if (!context) return;
262 safeWriteStr(fd, "\n--- CPU REGISTERS ---\n");
263 #if defined(__linux__) && defined(__x86_64__)
264 ucontext_t* uc = (ucontext_t*)context;
265 safeWriteStr(fd, "RAX: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RAX]);
266 safeWriteStr(fd, "RBX: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RBX]);
267 safeWriteStr(fd, "RCX: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RCX]);
268 safeWriteStr(fd, "RDX: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RDX]);
269 safeWriteStr(fd, "RSI: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RSI]);
270 safeWriteStr(fd, "RDI: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RDI]);
271 safeWriteStr(fd, "RBP: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RBP]);
272 safeWriteStr(fd, "RSP: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RSP]);
273 safeWriteStr(fd, "RIP: "); safeWriteHex(fd, uc->uc_mcontext.gregs[REG_RIP]);
274 #elif defined(_WIN32)
275 PCONTEXT ctx = ((PEXCEPTION_POINTERS)context)->ContextRecord;
276 safeWriteStr(fd, "RAX: "); safeWriteHex(fd, ctx->Rax);
277 safeWriteStr(fd, "RBX: "); safeWriteHex(fd, ctx->Rbx);
278 safeWriteStr(fd, "RCX: "); safeWriteHex(fd, ctx->Rcx);
279 safeWriteStr(fd, "RDX: "); safeWriteHex(fd, ctx->Rdx);
280 safeWriteStr(fd, "RSI: "); safeWriteHex(fd, ctx->Rsi);
281 safeWriteStr(fd, "RDI: "); safeWriteHex(fd, ctx->Rdi);
282 safeWriteStr(fd, "RBP: "); safeWriteHex(fd, ctx->Rbp);
283 safeWriteStr(fd, "RSP: "); safeWriteHex(fd, ctx->Rsp);
284 safeWriteStr(fd, "RIP: "); safeWriteHex(fd, ctx->Rip);
285 safeWriteStr(fd, "R8: "); safeWriteHex(fd, ctx->R8);
286 safeWriteStr(fd, "R9: "); safeWriteHex(fd, ctx->R9);
287 safeWriteStr(fd, "R10: "); safeWriteHex(fd, ctx->R10);
288 safeWriteStr(fd, "R11: "); safeWriteHex(fd, ctx->R11);
289 #endif
290 }
291
292 static void writeCrashReport(const char* reason, void* context, uintptr_t manualAddr = 0) {
293 bool expected = false;
294 if (!gIsCrashing.compare_exchange_strong(expected, true)) return;
295
296 int fd = gCrashLogFd;
297 uintptr_t crashAddr = manualAddr;
298
299 #if defined(__linux__) && defined(__x86_64__)
300 if (context) crashAddr = (uintptr_t)((ucontext_t*)context)->uc_mcontext.gregs[REG_RIP];
301 #elif defined(_WIN32)
302 if (context) crashAddr = (uintptr_t)((PEXCEPTION_POINTERS)context)->ExceptionRecord->ExceptionAddress;
303 #endif
304
305 if (fd >= 0) {
306 safeWriteStr(fd, "\n========== CRASH OCCURRED ==========\nReason: ");
307 safeWriteStr(fd, reason);
308 safeWriteStr(fd, "\n");
309
310 if (crashAddr != 0) {
311 safeWriteStr(fd, "Absolute Crash Address: ");
312 safeWriteHex(fd, crashAddr);
313 }
314
315 dumpRegisters(fd, context);
316
318
319 dumpProcessMap(fd);
320
321 safeWriteStr(fd, "\n--- RECENT LOGS ---\n");
322 size_t idx = gRingHead;
323 for(size_t k=0; k<gRingBuffer.size(); ++k) {
324 const auto& e = gRingBuffer[idx];
325 if(e.m_msg[0] != '\0') {
326 safeWriteStr(fd, "["); safeWriteStr(fd, e.m_time); safeWriteStr(fd, "] ");
327 safeWriteStr(fd, "["); safeWriteStr(fd, e.m_level); safeWriteStr(fd, "] ");
328 safeWriteStr(fd, e.m_msg); safeWriteStr(fd, "\n");
329 }
330 idx = (idx + 1) % gRingBuffer.size();
331 }
332
333 const char* msg = "\n[CRITICAL] CRASH DUMP SAVED TO LOG.\n";
334 WRITE_FUNC(2, msg, 36);
335
336 SYNC_FUNC(fd);
337
338 std::this_thread::sleep_for(std::chrono::seconds(1));
339
340 std::filesystem::path logPath = GetLogDir() / "game_session.log";
341 showCrashDialog(logPath.string());
342 } else {
343 const char* msg = "\n[CRITICAL] NO CRASH FILE OPEN.\n";
344 WRITE_FUNC(2, msg, 31);
345 }
346 }
347
348#if defined(_WIN32)
349 LONG WINAPI WindowsCrashHandler(EXCEPTION_POINTERS* p) {
350 writeCrashReport("Exception (Windows)", p);
351 #if DEBUG
352 std::string trace_str = cpptrace::generate_trace().to_string();
353 log(LogLevel::CRITICAL, "\n--- VEX ENGINE STACKTRACE ---\n%s\n",
354 trace_str.c_str());
355 #endif
356 return EXCEPTION_EXECUTE_HANDLER;
357 }
358#else
359 void posixSignalHandler(int sig, siginfo_t* info, void* context) {
360 const char* name = "Unknown";
361 if(sig == SIGSEGV) {
362 if (info && info->si_code == SEGV_MAPERR) name = "SIGSEGV (Address not mapped)";
363 else if (info && info->si_code == SEGV_ACCERR) name = "SIGSEGV (Access denied)";
364 else name = "SIGSEGV";
365 }
366 else if(sig == SIGABRT) name = "SIGABRT";
367 else if(sig == SIGILL) name = "SIGILL";
368 else if(sig == SIGFPE) name = "SIGFPE";
369
370 writeCrashReport(name, context);
371
372 #if DEBUG
373 cpptrace::generate_trace().print();
374 struct sigaction sa;
375 sa.sa_handler = SIG_DFL;
376 sigemptyset(&sa.sa_mask);
377 sa.sa_flags = 0;
378 sigaction(sig, &sa, nullptr);
379 raise(sig);
380 #else
381 _exit(1);
382 #endif
383 }
384#endif
385
387 if(gCrashLogFd >= 0) return;
388
389 std::filesystem::path logPath = GetLogDir() / "game_session.log";
390 std::string pathStr = logPath.string();
391
392 gCrashLogFd = OPEN_FUNC(pathStr.c_str(), O_FLAGS, S_FLAGS);
393
394 if (gCrashLogFd >= 0) {
395 safeWriteStr(gCrashLogFd, "=== VEX ENGINE SESSION START ===\n");
396 const char* msg = "[SYSTEM] Crash Handler Initialized.\n";
397 WRITE_FUNC(2, msg, 32);
398 } else {
399 fprintf(stderr, "[ERROR] Failed to open persistent log file at: %s\n", pathStr.c_str());
400 }
401
402#if defined(_WIN32)
403 SetUnhandledExceptionFilter(WindowsCrashHandler);
404#else
405 struct sigaction sa;
406 memset(&sa, 0, sizeof(sa));
407 sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
408 sa.sa_sigaction = posixSignalHandler;
409 sigaction(SIGSEGV, &sa, nullptr);
410 sigaction(SIGABRT, &sa, nullptr);
411 sigaction(SIGILL, &sa, nullptr);
412 sigaction(SIGFPE, &sa, nullptr);
413#endif
414 }
415
416 static void logInternal(LogLevel level, const char* fmt, va_list args) {
417 va_list argsCopy;
418 va_copy(argsCopy, args);
419 int len = vsnprintf(nullptr, 0, fmt, argsCopy);
420 va_end(argsCopy);
421
422 if (len < 0) return;
423
424 std::vector<char> buffer(len + 1);
425 vsnprintf(buffer.data(), buffer.size(), fmt, args);
426
427 pushToRing(level, buffer.data());
428
429 {
430 std::lock_guard<std::mutex> lock(gCallbackMutex);
431 for (auto& callback : gLogCallbacks) {
432 callback(level, buffer.data());
433 }
434 }
435
436 bool quietTerminal = false;
437 bool skipFile = false;
438
439 #if !DEBUG
440 if (level == LogLevel::INFO) {
441 skipFile = true;
442 }
443
444 if (level == LogLevel::INFO || level == LogLevel::WARNING) {
445 quietTerminal = true;
446 }
447
448 #ifdef DIST_BUILD
449 quietTerminal = true;
450 #endif
451 #endif
452
453 if (gCrashLogFd >= 0 && !skipFile) {
454 std::string timeStr = getTimeStr();
455 const char* levelStr = getLevelStr(level);
456 safeWriteStr(gCrashLogFd, "["); safeWriteStr(gCrashLogFd, timeStr.c_str()); safeWriteStr(gCrashLogFd, "] ");
457 safeWriteStr(gCrashLogFd, "["); safeWriteStr(gCrashLogFd, levelStr); safeWriteStr(gCrashLogFd, "] ");
458 safeWriteStr(gCrashLogFd, buffer.data()); safeWriteStr(gCrashLogFd, "\n");
459 }
460
461 if (!quietTerminal) {
462 std::string timeStr = getTimeStr();
463 const char* levelStr = getLevelStr(level);
464 SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO,
465 "%s [%s] %s", timeStr.c_str(), levelStr, buffer.data());
466 }
467 }
468
469#if DEBUG
470 [[noreturn]] void throw_error(const std::string& msg) {
471 throw cpptrace::runtime_error(msg.c_str());
472 }
473 void log(const char* fmt, ...) {
474 va_list args; va_start(args, fmt);
475 logInternal(LogLevel::INFO, fmt, args); va_end(args);
476 }
477 void log(LogLevel level, const char* fmt, ...) {
478 va_list args; va_start(args, fmt);
479 logInternal(level, fmt, args); va_end(args);
480 }
481 void handle_exception(const std::exception& e) {
482 if(auto* cpptr = dynamic_cast<const cpptrace::exception*>(&e)) {
483 cpptr->trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
484 } else {
485 log(LogLevel::CRITICAL, "%s", e.what());
486 }
487 throw;
488 }
489 void handle_critical_exception(const std::exception& e) {
490 if(auto* cpptr = dynamic_cast<const cpptrace::exception*>(&e)) {
491 cpptr->trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
492 } else {
493 log(LogLevel::CRITICAL, "%s", e.what());
494 }
495 throw;
496 }
497#else
498 [[noreturn]] void throw_error(const std::string& msg) {// [CHANGE] Capture the address of the code that called this function
499 uintptr_t callerAddress = 0;
500
501 #ifdef _MSC_VER
502 callerAddress = (uintptr_t)_ReturnAddress();
503 #else
504 callerAddress = (uintptr_t)__builtin_return_address(0);
505 #endif
506
507 writeCrashReport(msg.c_str(), nullptr, callerAddress);
508 if (gCrashLogFd >= 0) CLOSE_FUNC(gCrashLogFd);
509 _exit(1);
510 }
511 void log(const char* fmt, ...) {
512 va_list args; va_start(args, fmt);
513 logInternal(LogLevel::INFO, fmt, args); va_end(args);
514 }
515 void log(LogLevel level, const char* fmt, ...) {
516 va_list args; va_start(args, fmt);
517 logInternal(level, fmt, args); va_end(args);
518 }
519 void handle_exception(const std::exception& e) {
520 pushToRing(LogLevel::ERROR, e.what());
521 #ifndef DIST_BUILD
522 SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "%s", e.what());
523 #endif
524 }
525 void handle_critical_exception(const std::exception& e) {
526 uintptr_t callerAddress = 0;
527 #ifdef _MSC_VER
528 callerAddress = (uintptr_t)_ReturnAddress();
529 #else
530 callerAddress = (uintptr_t)__builtin_return_address(0);
531 #endif
532
533 writeCrashReport(e.what(), nullptr, callerAddress);
534 if (gCrashLogFd >= 0) CLOSE_FUNC(gCrashLogFd);
535 #ifndef DIST_BUILD
536 SDL_LogCritical(SDL_LOG_CATEGORY_ERROR, "%s", e.what());
537 #endif
538 _exit(1);
539 }
540#endif
541
542}
static void PrintCrashDump(int fd)
Prints hardware information to a crash dump file descriptor.
void(* LogCallbackFn)(LogLevel level, const char *message)
Defines the callback signature.
void VEX_EXPORT AddLogCallback(LogCallbackFn callback)
Adds a callback function to be called when a log message is generated.
void VEX_EXPORT InitCrashHandler()
Initializes the low-level crash handler.
std::filesystem::path VEX_EXPORT GetLogDir()
Gets the directory where logs and crash dumps are stored.
Definition PathUtils.cpp:95
void VEX_EXPORT throw_error(const std::string &msg)
Throws an error or terminates the application.
void VEX_EXPORT log(const char *fmt,...)
Logs a formatted message.
void VEX_EXPORT handle_critical_exception(const std::exception &e)
Handles a critical exception, forcing termination.
void VEX_EXPORT handle_exception(const std::exception &e)
Handles an exception based on build configuration.
LogLevel
Enum class for log levels.