1#include "components/ErrorUtils.hpp"
2#include "components/PathUtils.hpp"
3#include "components/HardwareInfo.hpp"
10#include <SDL3/SDL_log.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
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
50#include <cpptrace/cpptrace.hpp>
54 static std::vector<LogCallbackFn> gLogCallbacks;
55 static std::mutex gCallbackMutex;
58 std::lock_guard<std::mutex> lock(gCallbackMutex);
59 gLogCallbacks.push_back(callback);
62 static void showCrashDialog(
const std::string& logPath) {
64 char tempPath[MAX_PATH];
65 GetTempPathA(MAX_PATH, tempPath);
66 std::string psScriptPath = std::string(tempPath) +
"vex_crash_reporter.ps1";
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"
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"
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"
91 "if (Test-Path '" + logPath +
"') {\n"
92 " $textBox.Text = Get-Content '" + logPath +
"' -Raw\n"
94 " $textBox.Text = 'Error: Log file not found at " + logPath +
"'\n"
97 "$form.Controls.Add($textBox)\n"
100 FILE* fp = fopen(psScriptPath.c_str(),
"w");
102 fprintf(fp,
"%s", psCode.c_str());
105 ShellExecuteA(NULL,
"open",
"powershell.exe",
106 (
"-ExecutionPolicy Bypass -WindowStyle Hidden -File \"" + psScriptPath +
"\"").c_str(),
109 ShellExecuteA(NULL,
"open",
"notepad.exe", logPath.c_str(), NULL, SW_SHOWNORMAL);
112 #elif defined(__linux__)
117 execlp(
"zenity",
"zenity",
119 "--filename", logPath.c_str(),
120 "--title",
"Vex Engine Crash Reporter",
121 "--width=800",
"--height=600",
122 "--font=Monospace 10",
125 execlp(
"kdialog",
"kdialog",
126 "--title",
"Vex Engine Crash Reporter",
127 "--textbox", logPath.c_str(),
131 execlp(
"xterm",
"xterm",
"-hold",
"-e",
"cat", logPath.c_str(), (
char*)NULL);
144 static std::array<LogEntry, 200> gRingBuffer;
145 static size_t gRingHead = 0;
146 static std::mutex gRingMutex;
148 static std::atomic<bool> gIsCrashing(
false);
149 static int gCrashLogFd = -1;
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");
159 static const char* getLevelStr(
LogLevel 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";
169 static void pushToRing(
LogLevel level,
const char* msg) {
170 if(gIsCrashing)
return;
172 std::unique_lock<std::mutex> lock(gRingMutex, std::defer_lock);
173 if(!lock.try_lock())
return;
175 LogEntry& entry = gRingBuffer[gRingHead];
179 struct tm* timeinfo = localtime(&rawtime);
180 strftime(entry.m_time,
sizeof(entry.m_time),
"%H:%M:%S", timeinfo);
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';
186 gRingHead = (gRingHead + 1) % gRingBuffer.size();
189 static void safeWriteStr(
int fd,
const char* s) {
190 if (!s || fd < 0)
return;
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);
197 const char replacement =
'?';
198 WRITE_FUNC(fd, &replacement, 1);
203 static void safeWriteHex(
int fd, uintptr_t val) {
210 safeWriteStr(fd,
"0x0\n");
214 while (val > 0 && i > 1) {
215 uintptr_t digit = val % 16;
216 buf[i] = (digit < 10) ? (
'0' + digit) : (
'a' + (digit - 10));
222 WRITE_FUNC(fd, &buf[i-1], 32 - (i-1));
225 static void dumpProcessMap(
int outFd) {
226 safeWriteStr(outFd,
"\n--- MEMORY MAP (Base Addresses) ---\n");
228 int mapFd = OPEN_FUNC(
"/proc/self/maps", O_RDONLY, 0);
232 while ((bytesRead = READ_FUNC(mapFd, buf,
sizeof(buf))) > 0) {
233 WRITE_FUNC(outFd, buf, bytesRead);
237 #elif defined(_WIN32)
238 HANDLE hProcess = GetCurrentProcess();
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))) {
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");
257 safeWriteStr(outFd,
"-----------------------------------\n");
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);
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;
296 int fd = gCrashLogFd;
297 uintptr_t crashAddr = manualAddr;
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;
306 safeWriteStr(fd,
"\n========== CRASH OCCURRED ==========\nReason: ");
307 safeWriteStr(fd, reason);
308 safeWriteStr(fd,
"\n");
310 if (crashAddr != 0) {
311 safeWriteStr(fd,
"Absolute Crash Address: ");
312 safeWriteHex(fd, crashAddr);
315 dumpRegisters(fd, context);
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");
330 idx = (idx + 1) % gRingBuffer.size();
333 const char* msg =
"\n[CRITICAL] CRASH DUMP SAVED TO LOG.\n";
334 WRITE_FUNC(2, msg, 36);
338 std::this_thread::sleep_for(std::chrono::seconds(1));
340 std::filesystem::path logPath =
GetLogDir() /
"game_session.log";
341 showCrashDialog(logPath.string());
343 const char* msg =
"\n[CRITICAL] NO CRASH FILE OPEN.\n";
344 WRITE_FUNC(2, msg, 31);
349 LONG WINAPI WindowsCrashHandler(EXCEPTION_POINTERS* p) {
350 writeCrashReport(
"Exception (Windows)", p);
352 std::string trace_str = cpptrace::generate_trace().to_string();
353 log(LogLevel::CRITICAL,
"\n--- VEX ENGINE STACKTRACE ---\n%s\n",
356 return EXCEPTION_EXECUTE_HANDLER;
359 void posixSignalHandler(
int sig, siginfo_t* info,
void* context) {
360 const char* name =
"Unknown";
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";
366 else if(sig == SIGABRT) name =
"SIGABRT";
367 else if(sig == SIGILL) name =
"SIGILL";
368 else if(sig == SIGFPE) name =
"SIGFPE";
370 writeCrashReport(name, context);
373 cpptrace::generate_trace().print();
375 sa.sa_handler = SIG_DFL;
376 sigemptyset(&sa.sa_mask);
378 sigaction(sig, &sa,
nullptr);
387 if(gCrashLogFd >= 0)
return;
389 std::filesystem::path logPath =
GetLogDir() /
"game_session.log";
390 std::string pathStr = logPath.string();
392 gCrashLogFd = OPEN_FUNC(pathStr.c_str(), O_FLAGS, S_FLAGS);
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);
399 fprintf(stderr,
"[ERROR] Failed to open persistent log file at: %s\n", pathStr.c_str());
403 SetUnhandledExceptionFilter(WindowsCrashHandler);
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);
416 static void logInternal(
LogLevel level,
const char* fmt, va_list args) {
418 va_copy(argsCopy, args);
419 int len = vsnprintf(
nullptr, 0, fmt, argsCopy);
424 std::vector<char> buffer(len + 1);
425 vsnprintf(buffer.data(), buffer.size(), fmt, args);
427 pushToRing(level, buffer.data());
430 std::lock_guard<std::mutex> lock(gCallbackMutex);
431 for (
auto& callback : gLogCallbacks) {
432 callback(level, buffer.data());
436 bool quietTerminal =
false;
437 bool skipFile =
false;
440 if (level == LogLevel::INFO) {
444 if (level == LogLevel::INFO || level == LogLevel::WARNING) {
445 quietTerminal =
true;
449 quietTerminal =
true;
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");
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());
470 [[noreturn]]
void throw_error(
const std::string& msg) {
471 throw cpptrace::runtime_error(msg.c_str());
473 void log(
const char* fmt, ...) {
474 va_list args; va_start(args, fmt);
475 logInternal(LogLevel::INFO, fmt, args); va_end(args);
478 va_list args; va_start(args, fmt);
479 logInternal(level, fmt, args); va_end(args);
482 if(
auto* cpptr =
dynamic_cast<const cpptrace::exception*
>(&e)) {
483 cpptr->trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
485 log(LogLevel::CRITICAL,
"%s", e.what());
490 if(
auto* cpptr =
dynamic_cast<const cpptrace::exception*
>(&e)) {
491 cpptr->trace().print(std::cerr, cpptrace::isatty(cpptrace::stderr_fileno));
493 log(LogLevel::CRITICAL,
"%s", e.what());
499 uintptr_t callerAddress = 0;
502 callerAddress = (uintptr_t)_ReturnAddress();
504 callerAddress = (uintptr_t)__builtin_return_address(0);
507 writeCrashReport(msg.c_str(),
nullptr, callerAddress);
508 if (gCrashLogFd >= 0) CLOSE_FUNC(gCrashLogFd);
511 void log(
const char* fmt, ...) {
512 va_list args; va_start(args, fmt);
513 logInternal(LogLevel::INFO, fmt, args); va_end(args);
516 va_list args; va_start(args, fmt);
517 logInternal(level, fmt, args); va_end(args);
520 pushToRing(LogLevel::ERROR, e.what());
522 SDL_LogCritical(SDL_LOG_CATEGORY_ERROR,
"%s", e.what());
526 uintptr_t callerAddress = 0;
528 callerAddress = (uintptr_t)_ReturnAddress();
530 callerAddress = (uintptr_t)__builtin_return_address(0);
533 writeCrashReport(e.what(),
nullptr, callerAddress);
534 if (gCrashLogFd >= 0) CLOSE_FUNC(gCrashLogFd);
536 SDL_LogCritical(SDL_LOG_CATEGORY_ERROR,
"%s", e.what());
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.
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.