eq2go/old/common/debug.hpp
2025-08-06 19:00:30 -05:00

435 lines
11 KiB
C++

// Copyright (C) 2007 EQ2EMulator Development Team - GPL v3 License
#pragma once
#include <chrono>
#include <cstdint>
#include <fstream>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <unistd.h>
// Debug level configuration - controls verbosity of logging output
#ifndef EQDEBUG
#define EQDEBUG 1
#endif
#ifndef ThrowError
void CatchSignal(int);
#if defined(CATCH_CRASH) || defined(_EQDEBUG)
#define ThrowError(errstr) { std::cout << "Fatal error: " << errstr << " (" << __FILE__ << ", line " << __LINE__ << ")" << std::endl; LogWrite(WORLD__ERROR, 0, "Debug", "Thrown Error: %s (%s:%i)", errstr, __FILE__, __LINE__); throw errstr; }
#else
#define ThrowError(errstr) { std::cout << "Fatal error: " << errstr << " (" << __FILE__ << ", line " << __LINE__ << ")" << std::endl; LogWrite(WORLD__ERROR, 0, "Debug", "Thrown Error: %s (%s:%i)", errstr, __FILE__, __LINE__); CatchSignal(0); }
#endif
#endif
/**
* Main logging system for EQ2Emulator
* Provides file and console logging with multiple log levels and categories
*/
class EQEMuLog
{
public:
// Log category enumeration - defines different types of log messages
enum LogIDs {
Status = 0, // Server status messages (must stay first)
Normal, // General operational messages
Error, // Error conditions and failures
Debug, // Debug information for development
Quest, // Quest system specific messages
Commands, // Player command execution logs
MaxLogID // Total number of log categories
};
// Callback function types for custom log handling
using msgCallbackBuf = std::function<void(LogIDs id, const char *buf, int8_t size, int32_t count)>;
using msgCallbackFmt = std::function<void(LogIDs id, const char *fmt, va_list ap)>;
/**
* Constructor - initializes logging system with default settings
* Sets up file pointers, log status flags, and callback handlers
*/
EQEMuLog()
{
for (int i = 0; i < MaxLogID; i++) {
fp[i] = nullptr;
#if EQDEBUG >= 2
pLogStatus[i] = 1 | 2; // Enable file and console output for debug builds
#else
pLogStatus[i] = 0; // Disable logging for release builds
#endif
logCallbackFmt[i] = nullptr;
logCallbackBuf[i] = nullptr;
}
#if EQDEBUG < 2
// Configure specific log levels for release builds
pLogStatus[Status] = 3; // File + console
pLogStatus[Error] = 3; // File + console
pLogStatus[Debug] = 3; // File + console
pLogStatus[Quest] = 2; // Console only
pLogStatus[Commands] = 2; // Console only
#endif
}
/**
* Destructor - closes all open log files
* Ensures proper cleanup of file handles
*/
~EQEMuLog()
{
for (int i = 0; i < MaxLogID; i++) {
if (fp[i]) {
fp[i]->close();
delete fp[i];
}
}
}
/**
* Sets callback function for all log categories (formatted version)
* @param proc Callback function to handle formatted log messages
*/
void SetAllCallbacks(msgCallbackFmt proc)
{
for (int r = Status; r < MaxLogID; r++) {
SetCallback(static_cast<LogIDs>(r), proc);
}
}
/**
* Sets callback function for all log categories (buffer version)
* @param proc Callback function to handle raw buffer log messages
*/
void SetAllCallbacks(msgCallbackBuf proc)
{
for (int r = Status; r < MaxLogID; r++) {
SetCallback(static_cast<LogIDs>(r), proc);
}
}
/**
* Sets callback function for specific log category (formatted version)
* @param id Log category to set callback for
* @param proc Callback function to handle formatted messages
*/
void SetCallback(LogIDs id, msgCallbackFmt proc)
{
if (id >= MaxLogID) {
return;
}
logCallbackFmt[id] = proc;
}
/**
* Sets callback function for specific log category (buffer version)
* @param id Log category to set callback for
* @param proc Callback function to handle buffer messages
*/
void SetCallback(LogIDs id, msgCallbackBuf proc)
{
if (id >= MaxLogID) {
return;
}
logCallbackBuf[id] = proc;
}
/**
* Writes raw buffer data to log with timestamp
* @param id Log category to write to
* @param buf Raw data buffer to write
* @param size Size of each element in buffer
* @param count Number of elements to write
* @return True if write was successful
*/
bool writebuf(LogIDs id, const char *buf, int8_t size, int32_t count)
{
if (id >= MaxLogID) {
return false;
}
bool dofile = false;
if (pLogStatus[id] & 1) {
dofile = open(id);
}
if (!(dofile || pLogStatus[id] & 2)) {
return false;
}
std::lock_guard<std::mutex> lock(MLog[id]);
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto tm = *std::localtime(&time_t);
if (dofile) {
*fp[id] << std::format("{:04d} [{:04d}{:02d}{:02d} {:02d}:{:02d}:{:02d}] ",
getpid(), tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
fp[id]->write(buf, size * count);
*fp[id] << "\n";
fp[id]->flush();
}
if (logCallbackBuf[id]) {
logCallbackBuf[id](id, buf, size, count);
}
if (pLogStatus[id] & 2) {
auto& stream = (pLogStatus[id] & 8) ? std::cerr : std::cout;
stream << "[" << LogNames[id] << "] ";
stream.write(buf, size * count);
stream << "\n";
}
return true;
}
/**
* Writes formatted message to log with timestamp
* @param id Log category to write to
* @param fmt Printf-style format string
* @param ... Variable arguments for format string
* @return True if write was successful
*/
template<typename... Args>
bool write(LogIDs id, const std::string& fmt, Args&&... args)
{
if (id >= MaxLogID) {
return false;
}
bool dofile = false;
if (pLogStatus[id] & 1) {
dofile = open(id);
}
if (!(dofile || pLogStatus[id] & 2)) {
return false;
}
std::lock_guard<std::mutex> lock(MLog[id]);
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto tm = *std::localtime(&time_t);
std::string buffer;
if constexpr (sizeof...(args) > 0) {
buffer = std::vformat(fmt, std::make_format_args(args...));
} else {
buffer = fmt;
}
if (dofile) {
*fp[id] << std::format("[{:04d}{:02d}{:02d} {:02d}:{:02d}:{:02d}] {}\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, buffer);
fp[id]->flush();
}
if (pLogStatus[id] & 2) {
auto& stream = (pLogStatus[id] & 8) ? std::cerr : std::cout;
stream << std::format("[{:04d}{:02d}{:02d} {:02d}:{:02d}:{:02d}] [{}] {}\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, LogNames[id], buffer);
}
return true;
}
/**
* Dumps binary data in hexadecimal format with ASCII representation
* @param id Log category to write to
* @param data Binary data to dump
* @param size Number of bytes to dump
* @param cols Number of columns per row (default 16)
* @param skip Number of bytes to skip at beginning (default 0)
* @return True if dump was successful
*/
bool Dump(LogIDs id, const int8_t* data, int32_t size, int32_t cols = 16, int32_t skip = 0)
{
if (size == 0) {
return true;
}
if (id >= MaxLogID) {
return false;
}
bool dofile = false;
if (pLogStatus[id] & 1) {
dofile = open(id);
}
if (!(dofile || pLogStatus[id] & 2)) {
return false;
}
std::lock_guard<std::mutex> lock(MLog[id]);
write(id, "Dumping Packet: {}", size);
// Output data in hexadecimal format with ASCII representation
std::string ascii(cols, '\0');
int j = 0;
for (int32_t i = skip; i < size; i++) {
if ((i - skip) % cols == 0) {
if (i != skip) {
writeNTS(id, dofile, " | {}\n", ascii);
}
writeNTS(id, dofile, "{:4}: ", i - skip);
ascii.assign(cols, '\0');
j = 0;
} else if ((i - skip) % (cols / 2) == 0) {
writeNTS(id, dofile, "- ");
}
writeNTS(id, dofile, "{:02X} ", static_cast<unsigned char>(data[i]));
if (data[i] >= 32 && data[i] < 127) {
ascii[j++] = data[i];
} else {
ascii[j++] = '.';
}
}
// Pad final line if necessary
int32_t k = ((size - skip) - 1) % cols;
if (k < 8) {
writeNTS(id, dofile, " ");
}
for (int32_t h = k + 1; h < cols; h++) {
writeNTS(id, dofile, " ");
}
writeNTS(id, dofile, " | {}\n", ascii.substr(0, j));
if (dofile) {
fp[id]->flush();
}
return true;
}
private:
/**
* Opens log file for specified category
* @param id Log category to open file for
* @return True if file opened successfully
*/
bool open(LogIDs id)
{
if (id >= MaxLogID) {
return false;
}
std::lock_guard<std::mutex> lock(MOpen);
if (pLogStatus[id] & 4) { // File error state
return false;
}
if (fp[id]) { // Already open
return true;
}
std::string exename;
#if defined(WORLD)
exename = "_world";
#elif defined(ZONE)
exename = "_zone";
#endif
std::string filename = std::format("{}{}_{:04d}.log", FileNames[id], exename, getpid());
fp[id] = new std::ofstream(filename, std::ios::app);
if (!fp[id]->is_open()) {
std::cerr << "Failed to open log file: " << filename << std::endl;
pLogStatus[id] |= 4; // Set error state
delete fp[id];
fp[id] = nullptr;
return false;
}
*fp[id] << "---------------------------------------------\n";
return true;
}
/**
* Internal write function without timestamp or locking (Not Thread Safe)
* @param id Log category to write to
* @param dofile Whether to write to file
* @param fmt Format string
* @param args Format arguments
*/
template<typename... Args>
void writeNTS(LogIDs id, bool dofile, const std::string& fmt, Args&&... args)
{
std::string buffer;
if constexpr (sizeof...(args) > 0) {
buffer = std::vformat(fmt, std::make_format_args(args...));
} else {
buffer = fmt;
}
if (dofile) {
*fp[id] << buffer;
}
if (pLogStatus[id] & 2) {
auto& stream = (pLogStatus[id] & 8) ? std::cerr : std::cout;
stream << buffer;
}
}
// File management
std::mutex MOpen; // Mutex for file opening operations
std::mutex MLog[MaxLogID]; // Per-category logging mutexes
std::ofstream* fp[MaxLogID]; // File pointers for each log category
// Log status flags (bitwise)
// 1 = output to file, 2 = output to stdout, 4 = file error, 8 = use stderr
int8_t pLogStatus[MaxLogID];
// Callback function pointers for custom log handling
msgCallbackFmt logCallbackFmt[MaxLogID]; // Formatted message callbacks
msgCallbackBuf logCallbackBuf[MaxLogID]; // Buffer message callbacks
// Static configuration arrays
static constexpr const char* FileNames[MaxLogID] = {
"logs/eq2emu", "logs/eq2emu", "logs/eq2emu_error",
"logs/eq2emu_debug", "logs/eq2emu_quest", "logs/eq2emu_commands"
};
static constexpr const char* LogNames[MaxLogID] = {
"Status", "Normal", "Error", "Debug", "Quest", "Command"
};
};
#ifdef _EQDEBUG
/**
* Performance monitoring utility for debug builds
* Measures execution time between construction and destruction
*/
class PerformanceMonitor
{
public:
/**
* Constructor - starts timing measurement
* @param ip Pointer to variable to store elapsed time
*/
PerformanceMonitor(int64_t* ip) : p(ip), start_time(std::chrono::high_resolution_clock::now())
{
}
/**
* Destructor - calculates and stores elapsed time
*/
~PerformanceMonitor()
{
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
*p += duration.count();
}
private:
std::chrono::high_resolution_clock::time_point start_time; // Start time measurement
int64_t* p; // Pointer to elapsed time storage
};
#endif